BPVM 小食包 #14 - 后端魔法:语句变成字节码
后端是语句最终变成可执行字节码的地方。这是创建虚拟机将运行的实际指令的最终编译阶段。
BPVM 小食包 #14 - 后端魔法:语句变成字节码
本文内容基于Unreal Engine 5.6.0
BPVM 小食包 - 蓝图知识快速投喂!是蓝图到字节码系列的一部分。
最终转换
你的节点已经变成了语句。现在后端将它们转换为字节码:
1
2
语句 → 后端 → 字节码
(高级) → (编译器) → (虚拟机指令)
这是蓝图变成可执行的地方!
认识 FKismetCompilerVMBackend
后端是字节码工厂:
1
2
3
4
5
6
7
8
class FKismetCompilerVMBackend
{
FScriptBuilderBase ScriptBuilder; // 构建字节码
UBlueprint* Blueprint; // 我们正在编译的
void ConstructFunction(FKismetFunctionContext& Context);
void GenerateBytecode(Statement);
};
它接收语句并输出原始字节码!
构建过程
1
2
3
4
5
6
7
8
9
10
11
12
13
void ConstructFunction(FKismetFunctionContext& Context)
{
// 步骤 1: 创建函数头
StartFunction(Context.Function);
// 步骤 2: 处理每个语句
for (auto* Statement : Context.AllGeneratedStatements) {
GenerateBytecode(Statement);
}
// 步骤 3: 完成函数
EndFunction();
}
就像用乐高积木搭建 - 头部、主体、尾部!
语句到字节码映射
每种语句类型都变成特定的字节码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch (Statement->Type) {
case KCST_CallFunction:
// 发出函数调用字节码
Writer << EX_CallFunc;
Writer << FunctionPtr;
break;
case KCST_Assignment:
// 发出赋值字节码
Writer << EX_Let;
Writer << TargetProperty;
Writer << SourceValue;
break;
case KCST_Return:
// 发出返回字节码
Writer << EX_Return;
Writer << ReturnValue;
break;
}
每种语句类型都有一个字节码配方!
脚本构建器
FScriptBuilderBase 实际写入字节:
1
2
3
4
5
6
7
8
9
10
11
12
13
class FScriptBuilderBase
{
TArray<uint8> Script; // 字节码缓冲区
void EmitByte(uint8 Byte) {
Script.Add(Byte);
}
void EmitFunction(UFunction* Func) {
Script.Add(EX_CallFunc);
Script.Add(GetFunctionID(Func));
}
};
它实际上在写字节到缓冲区!
真实示例:Print String
观看完整转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 语句
KCST_CallFunction {
Function: PrintString
Param: "Hello"
}
// 后端生成
ScriptBuilder.EmitByte(EX_CallFunc); // 0x44
ScriptBuilder.EmitPointer(PrintString); // 函数地址
ScriptBuilder.EmitString("Hello"); // 参数
ScriptBuilder.EmitByte(EX_EndParams); // 0x50
// 最终字节码
[0x44][0x00001234]["Hello"][0x50]
跳转解析
后端解析跳转目标:
1
2
3
4
5
6
7
8
9
// 语句有标签
KCST_UnconditionalGoto {
Target: "Label_End"
}
// 后端转换为偏移量
uint32 JumpOffset = LabelOffsets["Label_End"];
ScriptBuilder.EmitByte(EX_Jump);
ScriptBuilder.EmitInt32(JumpOffset); // 实际字节偏移!
标签变成脚本中的字节偏移量!
生成期间的优化
后端可以在生成时优化:
1
2
3
4
5
6
7
8
9
10
// 相邻的跳转
if (LastInstruction == EX_Jump &&
CurrentInstruction == EX_Jump) {
// 合并为单个跳转!
}
// 返回后的死代码
if (LastInstruction == EX_Return) {
// 跳过所有内容直到下一个标签
}
最后一分钟的优化以获得更快的执行!
字节码缓冲区
最终产品只是一个字节数组:
1
2
3
4
5
6
7
8
9
10
UFunction* Function;
Function->Script.Empty();
// 填充生成的字节码
for (uint8 Byte : ScriptBuilder.GetScript()) {
Function->Script.Add(Byte);
}
// 现在 Function->Script 包含:
// [0x44][0x08]["Hello"][0x50][0x53]...
这个数组就是你编译的蓝图函数!
多个后端
虚幻可以有不同的后端:
1
2
3
4
5
6
7
8
// 虚拟机后端(默认)
FKismetCompilerVMBackend → 虚拟机的字节码
// C++ 后端(本地化)
FKismetCompilerCppBackend → C++ 源代码
// 调试后端
FKismetCompilerDebugBackend → 调试信息
相同的语句,不同的输出格式!
错误处理
后端捕获最终错误:
1
2
3
4
5
6
7
if (!Function) {
Error("无法发出对空函数的调用");
}
if (JumpOffset > MAX_OFFSET) {
Error("跳转太远!");
}
防止坏字节码的最后一道防线!
大小很重要
后端跟踪脚本大小:
1
2
3
4
5
6
7
8
9
10
// 之前
Function->Script.Num() = 0
// 后端之后
Function->Script.Num() = 2048 // 2KB 的字节码!
// 更大的函数 = 更慢的执行
if (Script.Num() > 10000) {
Warning("函数非常大,考虑拆分");
}
快速要点
- 后端是最终编译阶段
- 将语句转换为字节码(实际的虚拟机指令)
- FScriptBuilderBase 写入原始字节
- 解析标签到偏移量
- 可以在生成期间优化
- 不同的后端用于不同的输出(虚拟机、C++、调试)
- 创建虚拟机执行的 Script 数组
最终工厂
后端是所有内容汇聚的地方。你的可视化节点已经经过处理器,变成了语句,现在最终转换为让你的蓝图实际运行的原始字节码。这是编译流水线中的最终工厂!
想要更多细节?
完整的后端分解:
下一篇:使你的蓝图更快的优化!
🍿 BPVM 小食包系列
- ← #13: DAG 调度器
- #14: 后端魔法 ← 你在这里
- #15: 优化解释 →
本文由作者按照
CC BY 4.0
进行授权