BaseType.Resolve() yielding null for base types in different assembly

187 Views Asked by At

I have a method to get all members of a type using mono.cecil, all the way through the type hierarchy, but I've found that often the following statement returns null:

(asmType has type 'TypeDefinition')
TypeDefinition baseType = asmType.BaseType.Resolve();

Prior to this statement I've checked that asmType.BaseType is not null (and I know that the base type exists). I think it tends to return null when the base type and derived type are in different assemblies. Both assemblies are being examined by mono.cecil, so I know it's capable of finding and handling both, at least individually.

Is there some way to nudge cecil to correctly resolve the base type?

UPDATE: If I add this code, it works, but it is not practical as a general solution:

if (baseType == null)
{
    var test = AssemblyDefinition.ReadAssembly("<hard-coded explicit path>");
    baseType = test.MainModule.GetType(asmType.BaseType.FullName);
}

UPDATE 2:

I tried using:

AssemblyDefinition.ReadAssembly(asmType.BaseType.Module.FileName)

But 'FileName' for some reason is the file name of 'asmType' (derived type) and not the base type ?

UPDATE 3:

I was able to work-around the problem by using my own cache for AssemblyDefinition which only records non-null values. The mono.cecil code adds possibly null values into its cache in the DefaultAssemblyResolver class, even though its possible to resolve the assembly successfully at a later point after that first attempt. The cecil 'Resolve' method in the DefaultAssemblyResolver class will never resolve an assembly after an initial attempt produces null since it will always later return the cached null value.

2

There are 2 best solutions below

0
Dave Doknjas On BEST ANSWER

I found a better work-around - but this just looks in already-read assemblies and looks for the base class there. Very strange that mono.cecil doesn't do this consistently:

private static readonly Dictionary<string, AssemblyDefinition> _AssemblyDefinitions = new();

//whenever 'AssemblyDefinition.ReadAssembly' is called, add to _AssemblyDefinitions if non-null, with the key being the AssemblyDefinition FullName

private static TypeDefinition TypeReferenceToTypeDefinition(TypeReference typeReference)
{
    TypeDefinition typeDefinition = typeReference.Resolve();

    //mono.cecil may possibly return a null value from its cache in the DefaultAssemblyResolver.Resolve method:
    if (typeDefinition == null)
    {
        if (_AssemblyDefinitions.TryGetValue(typeReference.Scope.ToString(), out AssemblyDefinition foundAssemblyDefinition))
            typeDefinition = foundAssemblyDefinition.MainModule.GetType(typeReference.FullName);
    }

    return typeDefinition;
}

Debugging mono.cecil shows that in the 'DefaultAssemblyResolver' class, the 'cache' dictionary sometimes has an item added with the AssemblyDefinition being null, so for my specific case, 'Resolve' returned the null item from the cache. My own version of 'cache' had a non-null item (I don't add null values to my cache, so if ReadAssembly returns null once it's not doomed forever to be null). I haven't figured out why mono.cecil adds null items to its cache.

10
Vagaus On

I don't known which version of Cecil you are using, but looking in 0.1.14 code I see it is possible to reproduce this behavior at least in 3 different ways:

  1. Having a custom assembly resolver that fails to resolve the assembly containing the type.
  2. If the Scope of that type is null.
  3. If the Scope of that type references the wrong assembly.

Notice that if I don´t specify an assembly resolver (or set it to null) then Cecil throws an AssemblyResolutionException exception.

If you are changing these assemblies through Cecil(other than simply reading / processing) then my best bet is that you are hitting either 2 or 3.

BTOH, if you are simply reading the assemblies and you have custom assembly resolver, then 1 would be my best bet.

Below is an example code that I used to play with it. Basically it loads an assembly (libb) and gets a type which has a BaseType defined in another assembly (liba) that is not in the same folder as libb, i.e, Cecil will not be able to resolve that assembly.

using System;
using Mono.Cecil;

IAssemblyResolver ar = null;
if (args.Length > 0 && args[0] == "resolver")
{
    Console.WriteLine("Using bad assembly resolver...");
    ar = new MyAssemblyResolver();
}
else
{
    Console.WriteLine("Using default assembly resolver...");
}

var a = AssemblyDefinition.ReadAssembly("../libb/bin/Debug/net6.0/libb.dll", new ReaderParameters() { AssemblyResolver = ar } );
var t = a.MainModule.GetType("libb.InLibB");

if (args.Length > 0 && args[0] == "null")
{
    Console.WriteLine("Setting scope to null.");
    t.BaseType.Scope = null;
}

if (args.Length > 0 && args[0] == "ms")
{
    Console.WriteLine($"Setting scope to {a.MainModule.TypeSystem.Object.Scope}.");
    t.BaseType.Scope = a.MainModule.TypeSystem.Object.Scope;
}

Console.WriteLine($"Type = {t.FullName}");
Console.WriteLine($"BaseType = {t.BaseType.FullName}");
Console.WriteLine($"BaseType.Resolve() != null: {t.BaseType.Resolve() != null}");


class MyAssemblyResolver : Mono.Cecil.BaseAssemblyResolver
{
    public override AssemblyDefinition Resolve (AssemblyNameReference name, ReaderParameters parameters) => null;
}