Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PersistedAssemblyBuilder generates invalid IL when loading fields with generic type parameters #110247

Open
jgh07 opened this issue Nov 28, 2024 · 6 comments · May be fixed by #110372
Open

PersistedAssemblyBuilder generates invalid IL when loading fields with generic type parameters #110247

jgh07 opened this issue Nov 28, 2024 · 6 comments · May be fixed by #110372
Assignees
Labels
area-System.Reflection.Emit bug in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@jgh07
Copy link

jgh07 commented Nov 28, 2024

Description

I have found a possible issue with the new PersistedAssemblyBuilder introduced in .NET 9 that causes invalid IL to be generated when loading fields of generic types that have a generic type parameter as the field type.

When decompiling the generated IL using ildasm or similar tools, the field reference shows up as having the concrete type instead of the type parameter.

Reproduction Steps

The following code reproduces the problem in .NET 9. It doesn't produce a working executable by itself since I did not set the entry point, so to execute code from the generated assembly, you would need to reference it by some other assembly.

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

PersistedAssemblyBuilder ab = new(new("a"), typeof(object).Assembly);
ModuleBuilder mb = ab.DefineDynamicModule("a");
TypeBuilder tb = mb.DefineType("Program");
MethodBuilder methb = tb.DefineMethod("Main", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(void), []);

ILGenerator il = methb.GetILGenerator();
il.Emit(OpCodes.Newobj, typeof(C<int>).GetConstructor([]));
il.Emit(OpCodes.Ldfld, typeof(C<int>).GetField("F"));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", BindingFlags.Public | BindingFlags.Static, null, [typeof(int)], []));
il.Emit(OpCodes.Ret);

tb.CreateType();
ab.Save("a.dll");

public class C<T>
{
    public C() { }
    public T F;
}

Expected behavior

The following IL should be emitted:

newobj instance void class [Test]C`1<int32>::.ctor()
ldfld !0 class [Test]C`1<int32>::F
call void [mscorlib]System.Console::WriteLine(int32)
ret

(Above is IL generated when running the code example on .NET 4.8.1, which is why it is using mscorlib.)
When the code is executed, the following output should be printed:

0

Actual behavior

The following IL is generated:

newobj instance void class [Test]C`1<int32>::.ctor()
ldfld int32 class [Test]C`1<int32>::F
call void [System.Console]System.Console::WriteLine(int32)
ret

As you can see, the second line references the type int32 directly, instead of the type parameter !0. This causes the following exception to be thrown when this code is exectued:

Unhandled exception. System.MissingFieldException: Field not found: 'C`1.F'.
   at Program.Main()

Regression?

API was newly introduced in .NET 9. On .NET 4.8.1, when using the regular AssemblyBuilder, it generates a working assembly.

Known Workarounds

No response

Configuration

  • .NET 9.0.100
  • Windows 11 23H2
  • Debug, x64

Other information

I asked a question on the topic on StackOverflow, a user provided an explanation which goes into more depth than I would have been able to.

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Nov 28, 2024
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-reflection
See info in area-owners.md if you want to be subscribed.

Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-reflection-emit
See info in area-owners.md if you want to be subscribed.

@steveharter steveharter added bug and removed untriaged New issue has not been triaged by the area owner labels Dec 2, 2024
@steveharter steveharter added this to the 10.0.0 milestone Dec 2, 2024
@steveharter
Copy link
Member

Verified repro; also verified that .NET Framework's implementation of AssemblyBuilder.Save() works correctly (with ldfld !0 class [Test]C`1<int32>::F).

@steveharter
Copy link
Member

steveharter commented Dec 2, 2024

I'm not sure this is a bug even though it works in .NET Framework with AssemblyBuilder.Save();

The example closes the declaring type and then obtains the field from that:

il.Emit(OpCodes.Ldfld, typeof(C<int>).GetField("F"));

I assume that instead the declaring type leaves its generic parameter open:

il.Emit(OpCodes.Ldfld, typeof(C<>).GetField("F"));

@steveharter steveharter added the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Dec 2, 2024
@jkotas
Copy link
Member

jkotas commented Dec 2, 2024

I'm not sure this is a bug

This looks like a bug to me.

il.Emit(OpCodes.Ldfld, typeof(C<>).GetField("F"));

This would not work. ldfld token has to point to instantiated generic type for the IL to be valid.

@steveharter
Copy link
Member

Yeah after trying that, I see that using the unbound generic field it generates:
ldfld !0 [Test]C`1::F
and not the expected
ldfld !0 class [Test]C`1<int32>::F

@steveharter steveharter removed the needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration label Dec 2, 2024
@steveharter steveharter self-assigned this Dec 2, 2024
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Dec 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.Reflection.Emit bug in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants