Skip to content

Reflection & System.Runtime [& self references?] #895

@jonpryor

Description

@jonpryor

In the "spirit" of #524 and #646 and probably others…

Under .NET, Is there a "correct" way to use DefaultReflectionImporter and have the output assembly reference System.Runtime and not System.Private.CoreLib?

Code of interest:

delegateDef.BaseType = module.ImportReference (typeof (MulticastDelegate));

Example: cecil-import-reflection.zip

Consider the cecil-import-reflection example:

% dotnet build
% ikdasm -assemblyref bin/Debug/net7.0/cecil-import-reflection.dll | grep Name=
	Name=System.Runtime
	Name=System.Runtime.Loader
	Name=Mono.Cecil
	Name=System.Console

Note that the default dotnet build output references System.Runtime. System.Private.CoreLib doesn't make an appearance.

Let's run the example, which uses Cecil to read an input assembly, add a new delegate type to the assembly, and write it out:

% dotnet run bin/Debug/net7.0/cecil-import-reflection.dll out.dll
% ikdasm -assemblyref out.dll | grep Name=
	Name=System.Runtime
	Name=System.Runtime.Loader
	Name=Mono.Cecil
	Name=System.Console
	Name=cecil-import-reflection
	Name=System.Private.CoreLib

Note that System.Private.CoreLib now exists as an assembly reference. (Also note that cecil-import-reflection is an assembly reference! See "Question 2", below.)

Question 1: Is there a way to not have System.Private.CoreLib added as an assembly reference? I tried playing around with a DefaultReflectionImporter subclass and had no luck with that, for reasons I wasn't able to understand.

What I did have luck with was:

  1. Write the assembly to a stream/disk.
  2. Read (1) via AssemblyDefinition.ReadAssembly()
  3. Modify AssemblyDefinition.MainModule.AssemblyReferences and AssemblyDefinition.MainModule.MemberReferences so that .Scope uses System.Runtime.

Something like:

static void WriteKludge (AssemblyDefinition assemblyDef, string path, bool keepIntermediate)
{
    var c       = new MemoryStream ();
    assemblyDef.Write (c);
    c.Position  = 0;

    if (keepIntermediate) {
        using var intermediate = File.Create (path + ".cecil");
        c.WriteTo (intermediate);
        c.Position  = 0;
    }

    var rp = new ReaderParameters {
        InMemory    = true,
        ReadSymbols = false,
        ReadWrite   = true,
    };
    var newAsm              = AssemblyDefinition.ReadAssembly (c, rp);
    var module              = newAsm.MainModule;
    var systemRuntimeRef    = module.AssemblyReferences.FirstOrDefault (r => r.Name == "System.Runtime");
    var privateCorelibRef   = module.AssemblyReferences.FirstOrDefault (r => r.Name == "System.Private.CoreLib");

    if (systemRuntimeRef == null && privateCorelibRef != null) {
        throw new NotSupportedException ("Don't support assemblies which only reference System.Private.CoreLib and not System.Runtime.");
    }

    var selfRef             = module.AssemblyReferences.FirstOrDefault (r => r.Name == newAsm.Name.Name);
    foreach (var member in module.GetMemberReferences ()) {
        if (member.DeclaringType.Scope == privateCorelibRef) {
            member.DeclaringType.Scope = systemRuntimeRef;
            continue;
        }
        if (member.DeclaringType.Scope == selfRef) {
            member.DeclaringType.Scope = null;
            continue;
        }
    }
    foreach (var type in module.GetTypeReferences ()) {
        if (type.Scope == privateCorelibRef) {
            type.Scope = systemRuntimeRef;
            continue;
        }
        if (type.Scope == selfRef) {
            type.Scope = null;
            continue;
        }
    }
    module.AssemblyReferences.Remove (privateCorelibRef);
    if (selfRef != null) {
        module.AssemblyReferences.Remove (selfRef);
    }
    newAsm.Write (path);
}

Is this what I should be doing?

(Aside: I found that the member references & assembly references would change before vs. after AssemblyDefinition.Write(), which implied to me that the collections are "incomplete" until everything is serialized at .Write().)

Question 2: The example also uses Reflection to load a type from the assembly and use that with Cecil. In this particular case, it's creating something equivalent to:

delegate void MyDelegate(MyType type);

where MyType is resolved from the assembly being modified.

The result of this is that out.dll now references cecil-import-reflection, i.e. "itself":

% dotnet run bin/Debug/net7.0/cecil-import-reflection.dll cecil-import-reflection.dll
% ikdasm -assemblyref out.dll | grep Name=
	Name=System.Runtime
	Name=System.Runtime.Loader
	Name=Mono.Cecil
	Name=System.Console
	Name=cecil-import-reflection
	Name=System.Private.CoreLib

Note Name=cecil-import-reflection!

This is mostly "just weird", and WriteKludge() checks for this situation and removes the "self reference".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions