BPVM Snack Pack #14 - Backend Magic: Statements Become Bytecode
The backend is where statements finally become executable bytecode. It's the final compiler stage that creates the actual instructions the VM will run.
The content in this post is based on Unreal Engine 5.6.0
BPVM Snack Pack - Quick Blueprint knowledge drops! Part of the Blueprint to Bytecode series.
The Final Transformation
Your nodes have become statements. Now the backend turns them into bytecode:
1
2
Statements → Backend → Bytecode
(High-level) → (Compiler) → (VM Instructions)
This is where Blueprint becomes executable!
Meet FKismetCompilerVMBackend
The backend is the bytecode factory:
1
2
3
4
5
6
7
8
class FKismetCompilerVMBackend
{
FScriptBuilderBase ScriptBuilder; // Builds bytecode
UBlueprint* Blueprint; // What we're compiling
void ConstructFunction(FKismetFunctionContext& Context);
void GenerateBytecode(Statement);
};
It takes statements and outputs raw bytecode!
The Construction Process
1
2
3
4
5
6
7
8
9
10
11
12
13
void ConstructFunction(FKismetFunctionContext& Context)
{
// Step 1: Create function header
StartFunction(Context.Function);
// Step 2: Process each statement
for (auto* Statement : Context.AllGeneratedStatements) {
GenerateBytecode(Statement);
}
// Step 3: Finalize function
EndFunction();
}
Like building with LEGO - header, body, footer!
Statement to Bytecode Mapping
Each statement type becomes specific bytecode:
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:
// Emit function call bytecode
Writer << EX_CallFunc;
Writer << FunctionPtr;
break;
case KCST_Assignment:
// Emit assignment bytecode
Writer << EX_Let;
Writer << TargetProperty;
Writer << SourceValue;
break;
case KCST_Return:
// Emit return bytecode
Writer << EX_Return;
Writer << ReturnValue;
break;
}
Each statement type has a bytecode recipe!
The Script Builder
FScriptBuilderBase actually writes the bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
class FScriptBuilderBase
{
TArray<uint8> Script; // The bytecode buffer
void EmitByte(uint8 Byte) {
Script.Add(Byte);
}
void EmitFunction(UFunction* Func) {
Script.Add(EX_CallFunc);
Script.Add(GetFunctionID(Func));
}
};
It’s literally writing bytes to a buffer!
Real Example: Print String
Watch the full transformation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Statement
KCST_CallFunction {
Function: PrintString
Param: "Hello"
}
// Backend generates
ScriptBuilder.EmitByte(EX_CallFunc); // 0x44
ScriptBuilder.EmitPointer(PrintString); // Function address
ScriptBuilder.EmitString("Hello"); // Parameter
ScriptBuilder.EmitByte(EX_EndParams); // 0x50
// Final bytecode
[0x44][0x00001234]["Hello"][0x50]
Jump Resolution
The backend resolves jump targets:
1
2
3
4
5
6
7
8
9
// Statement has labels
KCST_UnconditionalGoto {
Target: "Label_End"
}
// Backend converts to offsets
uint32 JumpOffset = LabelOffsets["Label_End"];
ScriptBuilder.EmitByte(EX_Jump);
ScriptBuilder.EmitInt32(JumpOffset); // Actual byte offset!
Labels become byte offsets in the script!
Optimization During Generation
The backend can optimize as it generates:
1
2
3
4
5
6
7
8
9
10
// Adjacent jumps
if (LastInstruction == EX_Jump &&
CurrentInstruction == EX_Jump) {
// Merge into single jump!
}
// Dead code after return
if (LastInstruction == EX_Return) {
// Skip everything until next label
}
Last-minute optimizations for faster execution!
The Bytecode Buffer
The final product is just an array of bytes:
1
2
3
4
5
6
7
8
9
10
UFunction* Function;
Function->Script.Empty();
// Fill with generated bytecode
for (uint8 Byte : ScriptBuilder.GetScript()) {
Function->Script.Add(Byte);
}
// Now Function->Script contains:
// [0x44][0x08]["Hello"][0x50][0x53]...
This array IS your compiled Blueprint function!
Multiple Backends
Unreal can have different backends:
1
2
3
4
5
6
7
8
// VM Backend (default)
FKismetCompilerVMBackend → Bytecode for VM
// C++ Backend (nativization)
FKismetCompilerCppBackend → C++ source code
// Debug Backend
FKismetCompilerDebugBackend → Debug information
Same statements, different output formats!
Error Handling
The backend catches final errors:
1
2
3
4
5
6
7
if (!Function) {
Error("Cannot emit call to null function");
}
if (JumpOffset > MAX_OFFSET) {
Error("Jump too far!");
}
Last line of defense against bad bytecode!
The Size Matters
Backend tracks script size:
1
2
3
4
5
6
7
8
9
10
// Before
Function->Script.Num() = 0
// After backend
Function->Script.Num() = 2048 // 2KB of bytecode!
// Bigger functions = slower execution
if (Script.Num() > 10000) {
Warning("Function very large, consider splitting");
}
Quick Takeaway
- The backend is the final compiler stage
- Turns statements into bytecode (actual VM instructions)
- FScriptBuilderBase writes raw bytes
- Resolves labels to offsets
- Can optimize during generation
- Different backends for different outputs (VM, C++, Debug)
- Creates the Script array that the VM executes
The Final Factory
The backend is where it all comes together. Your visual nodes have traveled through handlers, become statements, and now finally transform into the raw bytecode that makes your Blueprint actually run. It’s the final factory in the compilation pipeline!
Want More Details?
For complete backend breakdown:
Next: The optimizations that make your Blueprint faster!
🍿 BPVM Snack Pack Series
- ← #13: The DAG Scheduler
- #14: Backend Magic ← You are here
- #15: Optimizations Explained →