Skip to content

PersistedAssemblyBuilder: TypeLoadException with in method parameters #123857

@stakx

Description

@stakx

Description

I am using .NET 9+'s new PersistedAssemblyBuilder to dynamically create a class type that implements a given interface:

public interface IFoo
{
    void Method(in Span<byte> bytes);  // note the `in` modifier
}

So I do the following:

  1. emit my class type FooDynamicImpl implementing above interface, using System.Reflection.Emit
  2. bake the type via TypeBuilder.CreateType
  3. write the dynamic assembly to disk via PersistedAssemblyBuilder.Save
  4. load it back into the runtime (so I have a runnable type) with Assembly.LoadFrom
  5. try to retrieve the type with Assembly.GetType("FooDynamicImpl").

The last step causes the following exception to get thrown:

Unhandled exception. System.TypeLoadException: Signature of the body and declaration in a method implementation do not match. Type: FooDynamicImpl.


Here's some more details regarding generated IL which I've used to create below repro code.

IL emitted for `IFoo` by a current C# compiler
.class interface public abstract auto ansi beforefieldinit IFoo
{
  .method public hidebysig newslot abstract virtual 
          instance void Method([in] valuetype [System.Runtime]System.Span`1<uint8>& modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) bytes) cil managed
  {
    .param [1]
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
  }
}

Here's the IL that a current C# compiler would emit for a class implementation of the interface:

public class FooImpl : IFoo
{
    public void Method(in Span<byte> bytes)
    {
    }
}
IL emitted for `FooImpl` by a current C# compiler
.class public auto ansi beforefieldinit FooImpl
       extends [System.Runtime]System.Object
       implements IFoo
{
  .method public hidebysig specialname rtspecialname 
          instance void .ctor() cil managed
  {
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call instance void [System.Runtime]System.Object::.ctor()
    IL_0006:  ret
  }

  .method public hidebysig instance void 
          Method([in] valuetype [System.Runtime]System.Span`1<uint8>& bytes) cil managed
  {
    .param [1]
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
    .maxstack  8
    IL_0000:  ret
  }

  .method private hidebysig newslot virtual final 
          instance void IFoo.Method([in] valuetype [System.Runtime]System.Span`1<uint8>& modreq([System.Runtime]System.Runtime.InteropServices.InAttribute) bytes) cil managed
  {
    .param [1]
    .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 ) 
    .override IFoo::Method
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldarg.1
    IL_0002:  call instance void FooImpl::Method(valuetype [System.Runtime]System.Span`1<uint8>&)
    IL_0007:  ret
  }
}

Reproduction Steps

Run the following program – the TypeLoadException should happen during the last statement.

using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

var assemblyName = new AssemblyName("DynamicAssembly");
var assembly = new PersistedAssemblyBuilder(assemblyName, typeof(object).Assembly);
var module = assembly.DefineDynamicModule("DynamicAssembly.dll");

// class FooDynamicImpl : IFoo
var type = module.DefineType("FooDynamicImpl", TypeAttributes.Public | TypeAttributes.Class);
type.AddInterfaceImplementation(typeof(IFoo));

// public FooDynamicImpl() { }
var ctor = type.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

// public void Method(in Span<byte> bytes) { }
var method = type.DefineMethod("Method", MethodAttributes.Public | MethodAttributes.HideBySig);
method.SetSignature(
	returnType: typeof(void),
	returnTypeRequiredCustomModifiers: [],
	returnTypeOptionalCustomModifiers: [],
	parameterTypes: [typeof(Span<byte>).MakeByRefType()],
	parameterTypeRequiredCustomModifiers: [[]],
	parameterTypeOptionalCustomModifiers: [[]]);
var methodParameter = method.DefineParameter(1, ParameterAttributes.In, "bytes");
methodParameter.SetCustomAttribute(typeof(IsReadOnlyAttribute).GetConstructor(types: [])!, []);
var methodIL = method.GetILGenerator();
methodIL.Emit(OpCodes.Ret);

// void IFoo.Method(in Span<byte> bytes) => Method(bytes)
var fooMethod = type.DefineMethod("IFoo.Method", MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual);
type.DefineMethodOverride(fooMethod, typeof(IFoo).GetMethod("Method")!);
fooMethod.SetSignature(
    returnType: typeof(void),
    returnTypeRequiredCustomModifiers: [],
    returnTypeOptionalCustomModifiers: [],
    parameterTypes: [typeof(Span<byte>).MakeByRefType()],
    parameterTypeRequiredCustomModifiers: [[typeof(InAttribute)]],
    parameterTypeOptionalCustomModifiers: [[]]);
var fooMethodParameter = fooMethod.DefineParameter(1, ParameterAttributes.In, "bytes");
fooMethodParameter.SetCustomAttribute(typeof(IsReadOnlyAttribute).GetConstructor(types: [])!, []);
var fooMethodIL = fooMethod.GetILGenerator();
fooMethodIL.Emit(OpCodes.Ldarg_0);
fooMethodIL.Emit(OpCodes.Ldarg_1);
fooMethodIL.Emit(OpCodes.Call, method);
fooMethodIL.Emit(OpCodes.Ret);

// Bake type & save dynamic assembly to disk
_ = type.CreateType();
var outputFilePath = Path.GetFullPath("DynamicAssembly.dll");
assembly.Save(outputFilePath);

// load saved assembly back into the runtime and get `FooDynamicImpl` type:
var runnableAssembly = Assembly.LoadFrom(outputFilePath);
var runnableType = runnableAssembly.GetType("FooDynamicImpl"); // <= TypeLoadException happens here

public interface IFoo
{
    void Method(in Span<byte> bytes);
}

I've inspected the saved assembly with ILDASM, here's the IL for the emitted FooDynamicImpl class type:

IL emitted for `FooDynamicImpl` by the above code:
.class public auto ansi FooDynamicImpl
       extends [System.Private.CoreLib]System.Object
       implements [ConsoleApp1]IFoo
{
  .method public hidebysig specialname rtspecialname 
          instance void .ctor() cil managed
  {
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call instance void [System.Private.CoreLib]System.Object::.ctor()
    IL_0006:  ret
  }

  .method public hidebysig instance void 
          Method([in] valuetype [System.Private.CoreLib]System.Span`1<uint8>& bytes) cil managed
  {
    .param [1]
    .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor()
    .maxstack  8
    IL_0000:  ret
  }

  .method private hidebysig newslot virtual final 
          instance void IFoo.Method([in] valuetype [System.Private.CoreLib]System.Span`1<uint8>& modreq([System.Private.CoreLib]System.Runtime.InteropServices.InAttribute) bytes) cil managed
  {
    .param [1]
    .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor()
    .override [ConsoleApp1]IFoo::Method
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldarg.1
    IL_0002:  call instance void FooDynamicImpl::Method(valuetype [System.Private.CoreLib]System.Span`1<uint8>&)
    IL_0007:  ret
  }
}

There are only minor differences to a C# compiler-generated version of an equivalent type (those are: 1. missing custom attribute init data, and 2. type references using [System.Private.CoreLib] instead of [System.Runtime]) but those differences do not appear to be relevant – see the described workaround below.

Expected behavior

I'd expect the program to succeed without error (and the retrieved Type to be instantiable & usable).

Actual behavior

The program throws the exception initially mentioned:

Unhandled exception. System.TypeLoadException: Signature of the body and declaration in a method implementation do not match. Type: FooDynamicImpl.

Regression?

The described problem does not happen when emitting code using a regular AssemblyBuilder (and thus skipping the assembly save/load roundtrip). It only happens with PersistedAssemblyBuilder, AFAIK on all .NET versions that support it (9+).

Known Workarounds

The issue only appears to happen in the presence of an in parameter. If you replace it with a ref parameter, the exception vanishes:

  • In the IFoo.Method declaration, replace in with ref:

     public interface IFoo
     {
    -    void Method(in Span<byte> bytes);
    +    void Method(ref Span<byte> bytes);
     }
  • In the call to fooMethod.SetSignature, change this line of code:

     fooMethod.SetSignature(
         returnType: typeof(void),
         returnTypeRequiredCustomModifiers: [],
         returnTypeOptionalCustomModifiers: [],
         parameterTypes: [typeof(Span<byte>).MakeByRefType()],
    -    parameterTypeRequiredCustomModifiers: [[typeof(InAttribute)]],
    +    parameterTypeRequiredCustomModifiers: [[]],
         parameterTypeOptionalCustomModifiers: [[]]);

Configuration

  • .NET versions tested: both 9 and 10 (PersistedAssemblyBuilder requires at least .NET 9)
  • OS: Windows 11 Pro
  • Architecture: x64 (CPU: 12th Gen Intel Core i7-1265U)
  • I haven't tried other OSes, architectures, nor Blazor

Other information

I think we can also ignore the fact that a byref-like type Span<byte> is involved in my repro. The same exception would occur with a byte parameter.

Possibly related to signature handling of modreqs.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions