DynamicMethod in C#: Lightweight Code Generation

The DynamicMethod class in .NET provides a high-performance mechanism for generating and executing Common Intermediate Language (CIL) at runtime without the overhead of creating dynamic assemblies or saving files to disk. Part of the System.Reflection.Emit namespace, it serves as a lightweight alternative to full reflection, enabling tasks such as dynamic mapping, runtime serialization, and fast member invocation. This guide covers how to instantiate a DynamicMethod, emit intermediate language instructions, compile the method into a typed delegate, and execute it efficiently.

Why Use DynamicMethod?

Standard reflection using PropertyInfo.GetValue or MethodInfo.Invoke incurs significant performance penalties due to runtime validation, boxing, and dispatch overhead. While compiling expression trees (Expression<Func<T>>) offers an alternative, expression trees introduce their own compilation overhead and do not support low-level IL instructions. DynamicMethod solves this by offering:

  • Minimal Overhead: Dynamic methods are hosted in lightweight, transient modules and can be garbage-collected when no longer referenced.
  • Direct IL Control: You emit exact opcodes, enabling fine-grained control over execution and memory.
  • Visibility Bypass: By specifying an associated module or type, a DynamicMethod can bypass standard access checks, allowing direct access to private or internal members.

Step-by-Step Implementation

Creating and executing a dynamic method involves four primary steps:

  1. Instantiate the DynamicMethod: Define its name, return type, parameter types, and owner context.
  2. Retrieve the ILGenerator: Use the generator to append IL opcodes.
  3. Emit IL Instructions: Push values onto the evaluation stack, execute operations, and return the result.
  4. Bake into a Delegate: Convert the generated bytecode into an invokable delegate using CreateDelegate.

Example: Generating an Arithmetic Function

The following example builds a dynamic function equivalent to (int a, int b) => a + b:

using System;
using System.Reflection.Emit;

public class Program
{
    public static void Main()
    {
        // 1. Define the dynamic method: name, return type, parameter types
        DynamicMethod addMethod = new DynamicMethod(
            name: "AddNumbers",
            returnType: typeof(int),
            parameterTypes: new Type[] { typeof(int), typeof(int) },
            m: typeof(Program).Module // Associated module context
        );

        // 2. Obtain an ILGenerator
        ILGenerator il = addMethod.GetILGenerator();

        // 3. Emit CIL instructions
        il.Emit(OpCodes.Ldarg_0); // Push first argument (a) onto the stack
        il.Emit(OpCodes.Ldarg_1); // Push second argument (b) onto the stack
        il.Emit(OpCodes.Add);     // Pop both, add them, and push the result
        il.Emit(OpCodes.Ret);     // Return the top value from the stack

        // 4. Create a strongly-typed delegate
        Func<int, int, int> addDelegate = (Func<int, int, int>)addMethod.CreateDelegate(typeof(Func<int, int, int>));

        // 5. Invoke the delegate
        int result = addDelegate(15, 27);
        Console.WriteLine($"Result: {result}"); // Output: 42
    }
}

Accessing Private Members

To access private or protected members, pass a target Type into the DynamicMethod constructor instead of a Module. This grants the dynamic method the same visibility permissions as the target class.

using System;
using System.Reflection;
using System.Reflection.Emit;

public class SecretVault
{
    private readonly string _secretCode = "XYZ-12345";
}

public class AccessorExample
{
    public static void Main()
    {
        FieldInfo field = typeof(SecretVault).GetField("_secretCode", BindingFlags.NonPublic | BindingFlags.Instance);

        // Associate the DynamicMethod directly with SecretVault to bypass private access checks
        DynamicMethod getter = new DynamicMethod(
            "GetSecretCode",
            typeof(string),
            new Type[] { typeof(SecretVault) },
            typeof(SecretVault),
            skipVisibility: true
        );

        ILGenerator il = getter.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);         // Load the instance argument
        il.Emit(OpCodes.Ldfld, field);     // Load the value of the private field
        il.Emit(OpCodes.Ret);              // Return the value

        Func<SecretVault, string> readSecret = (Func<SecretVault, string>)getter.CreateDelegate(typeof(Func<SecretVault, string>));

        SecretVault vault = new SecretVault();
        Console.WriteLine(readSecret(vault)); // Output: XYZ-12345
    }
}

Best Practices

  • Cache Delegates: Emitting IL and compiling delegates via CreateDelegate carries initial latency. Always cache the resulting delegate in a dictionary or static field to ensure subsequent invocations execute at native speed.
  • Handle the Evaluation Stack Correctly: Intermediate Language works strictly on an evaluation stack. Every method must exit with the correct number of items on the stack matching the method's return type (zero for void, one for non-void).
  • Match Types Exactly: Ensure opcodes match data types (e.g., using Ldc_I4 for 32-bit integers vs. Ldc_I8 for 64-bit integers) to avoid runtime verification exceptions.