BPVM Snack Pack #14 - Backend Magic: Statements Become Bytecode | ebp BPVM Snack Pack #14 - Backend Magic: Statements Become Bytecode | ebp BPVM Snack Pack #14 - Backend Magic: Statements Become Bytecode | ebp
Post

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.

BPVM Snack Pack #14 - Backend Magic: Statements Become Bytecode

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

This post is licensed under CC BY 4.0 by the author.