IL Post Process (ILPP) in Unity: How Ceres Eliminates Runtime Reflection for Maximum Performance
IL Post Process (ILPP) is a Unity compilation pipeline hook that manipulates Intermediate Language (IL) after the C# compiler finishes but before the assembly is written to disk, allowing the Ceres framework to generate initialization code at compile time and eliminate expensive runtime reflection.
The akikurisu/ceres repository leverages IL Post Processing to transform node-based gameplay logic into high-performance IL that avoids allocations and virtual dispatch. By operating on the assembly via Mono.Cecil during the build phase, Ceres converts reflection-heavy operations into direct method calls and field accesses.
What is IL Post Process (ILPP)?
IL Post Processing is a Unity Editor feature that intercepts compiled assemblies before they are packaged into the final build. Implementing the ILPostProcessor interface allows frameworks to rewrite IL instructions using libraries like Mono.Cecil, enabling compile-time code generation that would otherwise require slow reflection at runtime.
In Ceres, the ILPP system lives in the Unity.Ceres.ILPP.CodeGen namespace and executes automatically whenever Unity compiles the Ceres.dll assembly. This process happens transparently during development, meaning developers write standard C# node classes while the ILPP infrastructure injects performance-critical boilerplate behind the scenes.
The Three Core ILPP Processors in Ceres
Ceres implements three specialized processors that work together to optimize node initialization and execution. Each processor targets specific performance bottlenecks in the node graph architecture.
RuntimeAccessModifiersILPP
Located in Runtime/CodeGen/RuntimeAccessModifiersILPP.cs, this processor modifies the accessibility of critical CeresNode fields. It changes SharedVariables, Ports, and PortLists from internal to protected internal by setting IsFamilyOrAssembly to true on their FieldDefinition.
This accessibility shift allows generated initialization code to access these collections directly without exposing them publicly in the source API. The modification happens without altering the original C# source files, maintaining clean encapsulation while enabling IL-level optimizations.
CeresNodeILPP
The Runtime/CodeGen/CeresNodeILPP.cs processor performs the heavy lifting of node initialization. For every subclass of CeresNode discovered via type.IsSubclassOf(CodeGenHelpers.CeresNode_FullName), it generates three critical overrides:
__initializeVariables– AllocatesSharedVariablefields only when null and registers them in theSharedVariableslist__initializePorts– InstantiatesCeresPortfields and populates thePortsdictionary with name-to-instance mappings__getTypeName– Returns a constant string representing the concrete type name, eliminating virtual dispatch overhead
These generated methods use conditional allocation (if (field == null) field = new T()) to prevent unnecessary heap allocations during node reuse.
ExecutableReflectionILPP
Found in Runtime/CodeGen/ExecutableReflectionILPP.cs, this processor eliminates reflection from executable flow-graph functions and implementable events. It rewrites event handler invocations that would normally use MethodInfo.Invoke into direct method calls.
By resolving method references at compile time and emitting direct IL instructions, this processor removes the costly reflection pipeline entirely. The resulting code executes as fast as standard virtual method calls while maintaining the flexibility of the original event architecture.
How IL Post Process Enhances Runtime Performance
The IL Post Process pipeline delivers measurable performance improvements through five specific optimizations:
1. Compile-Time Code Generation
Instead of using reflection at runtime to discover SharedVariable and CeresPort fields via GetFields(), Ceres generates concrete __initializeVariables and __initializePorts methods during the build. This transforms runtime type inspection into simple method invocations that the JIT compiler can inline.
2. Allocation-Free Initialization
The generated IL only instantiates objects when fields are null, avoiding repeated heap allocations in tight gameplay loops. This pattern ensures that resetting or reusing nodes does not trigger garbage collection pressure.
3. Eliminated Virtual Dispatch
By overriding __getTypeName to return a constant string rather than relying on GetType().Name or hash lookups, Ceres enables the runtime to resolve type identity with zero overhead. The method becomes a sealed accessor returning a pre-computed literal.
4. Protected Member Access Without Source Pollution
RuntimeAccessModifiersILPP enables generated code to access internal node state without making fields public or modifying the original source. This maintains API cleanliness while allowing the IL-level optimizations to function.
5. Reflection-Free Event Handling
ExecutableReflectionILPP converts MethodInfo.Invoke calls into direct IL call instructions. This removes the expensive reflection binding and boxing overhead typically associated with dynamic event invocation in Unity.
The Ceres ILPP Architecture Flow
The IL Post Process execution follows a precise pipeline from compilation to final assembly:
-
Compilation Phase – Unity compiles C# source files into the
Ceres.dllassembly -
ILPP Discovery – Unity's build pipeline identifies classes implementing
ILPostProcessorin theUnity.Ceres.ILPP.CodeGennamespace -
Assembly Loading –
CodeGenHelpers.AssemblyDefinitionForloads the compiled assembly into Mono.Cecil objects for manipulation -
Module Resolution –
CodeGenHelpers.FindBaseModuleslocates the Unity core module and Ceres module to resolve type references -
Type Walking – Each processor iterates type definitions:
CeresNodeILPPidentifies node subclasses and injects initialization logicRuntimeAccessModifiersILPPtoggles accessibility flags on target fieldsExecutableReflectionILPPrewrites method bodies to remove reflection
-
Assembly Write-Back – The modified assembly and PDB are written to memory and returned to Unity for final packaging
Any errors or warnings during this process are collected in the m_Diagnostics list and surfaced in the Unity Console, providing clear feedback when code generation fails.
IL Post Process Code Generation Example
Before ILPP: User-Defined Node
Developers write clean, attribute-free C# classes:
public class MyNode : CeresNode
{
public SharedVariable<int> counter;
public CeresPort output;
}
After ILPP: Generated Implementation
The IL Post Process injects the following logic into the assembly (visible via IL inspection):
protected internal List<SharedVariable> SharedVariables;
protected internal Dictionary<string, CeresPort> Ports;
protected override void __initializeVariables()
{
if (counter == null)
counter = new SharedVariable<int>();
SharedVariables.Add(counter);
base.__initializeVariables();
}
protected override void __initializePorts()
{
if (output == null)
output = new CeresPort();
Ports.Add(nameof(output), output);
base.__initializePorts();
}
protected internal override string __getTypeName()
{
return "MyNode";
}
Runtime Usage: Zero Reflection
At runtime, the Ceres engine invokes these generated methods directly:
var node = new MyNode();
node.__initializeVariables(); // Direct call, no reflection
node.__initializePorts(); // Direct call, no reflection
Debug.Log(node.counter.Value); // Direct field access
Summary
-
IL Post Process (ILPP) manipulates assembly IL after C# compilation but before disk write, enabling compile-time optimizations in Unity.
-
Ceres uses three processors—
RuntimeAccessModifiersILPP,CeresNodeILPP, andExecutableReflectionILPP—located inRuntime/CodeGen/to transform node classes. -
The system eliminates runtime reflection by generating
__initializeVariables,__initializePorts, and__getTypeNamemethods during the build. -
Performance gains include allocation-free initialization, eliminated virtual dispatch, and reflection-free event handling.
-
All transformations occur via Mono.Cecil during the Unity build pipeline, requiring no runtime overhead or source code pollution.
Frequently Asked Questions
What does IL Post Process modify in Unity?
IL Post Process modifies the compiled Intermediate Language (IL) of managed assemblies after the C# compiler generates them but before Unity packages them into the build. In Ceres, it modifies Ceres.dll to inject initialization methods and adjust field accessibility using Mono.Cecil, transforming the assembly without changing the original source files.
How does ILPP improve performance compared to reflection?
ILPP improves performance by shifting work from runtime to compile time. Instead of using GetFields() and MethodInfo.Invoke during gameplay—which involves expensive metadata lookups and boxing—Ceres generates direct IL instructions that the JIT compiler can optimize and inline. This eliminates allocation overhead and virtual dispatch costs entirely.
Which Ceres components rely on IL Post Processing?
The node initialization system relies on CeresNodeILPP to set up SharedVariable and CeresPort instances, while RuntimeAccessModifiersILPP enables access to the SharedVariables, Ports, and PortLists collections. Additionally, ExecutableReflectionILPP optimizes the executable graph flow by removing reflection from event invocations.
Is ILPP specific to Ceres or a general Unity feature?
IL Post Processing is a general Unity Editor feature available to any package or project. Ceres implements specific ILPostProcessor subclasses to customize the pipeline for its node architecture. Other Unity packages like Entities and Netcode for GameObjects also use ILPP for similar compile-time code generation tasks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →