Skip to content

Instantly share code, notes, and snippets.

@buyaa-n
Created February 12, 2024 19:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buyaa-n/4fcf445fc2f642cfd055aa1ef3ef2772 to your computer and use it in GitHub Desktop.
Save buyaa-n/4fcf445fc2f642cfd055aa1ef3ef2772 to your computer and use it in GitHub Desktop.

System.Reflection.Emit: Saving generated assemblies in NET 9.0.

The AssemblyBuilder.Save method haven't ported to .NET Core with technical difficulties like its implementation heavily depend on windows specific native code that also not ported into .NET Core. Previously with .NET core developers can only run the generated assembly. Emitting assembly, IL is quite challenging work, without having an option to save an assembly, it is very difficult to debug in-memory assemblies. Saving the assembly to a file allows generated assemblies to be verified with tools such as ILVerify, or decompiled and manually examined with tools such as ILSpy. Furthermore, the saved assembly can be shared or loaded directly which can be used to decrease application startup time. Github issue for adding support for AssemblyBuilder.Save has been open for several years and it's been the most upvoted issue in the Reflection area. Many customers report it as a blocker for porting their project from .NET Framework, meanwhile a third party library alternative have been developed but not all customers have unblocked with that

In .NET 9.0 we are adding fully managed new Reflection.Emit implementation that supports saving. This implementation has no dependency from existing runtime specific Reflection.Emit implementation, i.e. now we have two different implementations in .NET Core. Generated assemblies with the existing runtime implementation can only run, the assemblies generated with new managed implementation can be saved, to run the assembly user should save it into a memory stream or a file first, then load it back.

For creating a new persisted AssemblyBuilder instance you should use the new static factory method AssemblyBuilder.DefinePersistedAssembly:

public static DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null);

The coreAssembly passed to the method used for resolving base runtime types and can be used for resolving reference assemblies versioning.

  • if Reflection.Emit is used to generate assembly that targets specific TFM, the reference assemblies for the given TFM should be opened using MetadataLoadContext and the value of MetadataLoadContext.CoreAssembly property should be used as the coreAssembly. It allows the generator to run on one .NET runtime version and target a different .NET runtime version.

  • If Reflection.Emit is used to generate an assembly that is only going to be executed on the same runtime version as the runtime version that the compiler is running on (typically in-proc), the core assembly can be typeof(object).Assembly. The reference assemblies are not necessary in this case.

Differences from runtime and .NET framework implementation:

  • The way of creating AssemblyBuilder instance is different for all implementation, use static factory method AssemblyBuilder.DefinePersistedAssembly(AssemblyName name, Assembly coreAssembly, IEnumerable<CustomAttributeBuilder>? assemblyAttributes = null) for persisted AssemblyBuilder. Further steps for defining a module/type/method/enum etc., and writing IL are same as existing runtime implementation.
  • The new persisted AssemblyBuilder implementation is only for saving, in order to run the assembly user first need to save it into a memory stream or a file, then load it back.
  • The metadata tokens for all members are populated on Save(...) operation, do not use the tokens of generated type and its members before saving as they will have default values or throw. It is safe to use tokens for types that are referenced, not generated.
  • Some APIs that are not improtant for emitting assembly are not implemented, for example GetCustomAttributes() is not implemented, with the runtime implementation you were able to use those APIs after creating the type, for persisted AssemblyBuilder they would throw NotSupportedException or NotImplementedException. If you have a scenario that needs those APIs implemented file an issue in the repo.

The following example demonstrates how to create and save assemblies:

public void CreateAndSaveAssembly(string assemblyPath)
{
    AssemblyBuilder ab = AssemblyBuilder.DefinePersistedAssembly(new AssemblyName("MyAssembly"), typeof(object).Assembly);
    TypeBuilder tb = ab.DefineDynamicModule("MyModule").DefineType("MyType", TypeAttributes.Public | TypeAttributes.Class);

    MethodBuilder mb = tb.DefineMethod("SumMethod", MethodAttributes.Public | MethodAttributes.Static,
        typeof(int), [typeof(int), typeof(int)]);
    ILGenerator il = mb.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldarg_1);
    il.Emit(OpCodes.Add);
    il.Emit(OpCodes.Ret);

    tb.CreateType();
    ab.Save(assemblyPath); // or could save to a Stream
}

The following example demonstrates how to run the saved assembly:

public void UseAssembly(string assemblyPath)
{
    Assembly assembly = Assembly.LoadFrom(assemblyPath);
    Type type = assembly.GetType("MyType");
    MethodInfo method = type.GetMethod("SumMethod");
    Console.WriteLine(method.Invoke(null, [5, 10]));
}

Now the implementation is available in preview 1, please try it out and give us a feedback. If your application employ complex generics and/or uses events you might hit failures, those are [fixed] (dotnet/runtime#97350) and will be available in preview 2. Further we will add entry point support.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment