BPVM 小食包 #14 - 后端魔法:语句变成字节码 | ebp BPVM 小食包 #14 - 后端魔法:语句变成字节码 | ebp BPVM 小食包 #14 - 后端魔法:语句变成字节码 | ebp
文章

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 小食包系列

本文由作者按照 CC BY 4.0 进行授权