Type equivalence and assembly identities

Topics: Metadata Model
Mar 10, 2011 at 7:54 PM

I am trying to wrap my head around how type equivalence works, and need some help...

I am running into a case where TypeHelper.TypesAreEquivalent(ref0, ref1) is returning a different value than TypeHelper.TypesAreEquivalent(ref0.ResolvedType, ref1.ResolvedType)

Is this normal, expected? Stepping through it, it seems like the reference versions are using the assembly identity to compute the intern key, which in this case is mscorlib 2.0.0.0, whereas the resolved definition versions are using the actual assembly name, which in this case is mscorlib 4.0.0.0.

Assuming this is intended, what are the best practices for comparing things for equivalence? Should you always resolve first? Or is there some way to not get into this situation?

 

Thanks,

~Jacob

Coordinator
Mar 11, 2011 at 2:23 PM

I don't think this is particularly normal or expected. There is perhaps an explanation, but I suspect a bug.

The thing to bear in mind is that a reference to type T in assembly A can be redirected to type S in assembly B, via type aliases. It is therefore possible that a reference and its resolved type have different identities for the purpose of TypesAreEquivalent.

I would not expect, however, that two references R1 and R2 that are considered equivalent, could resolve to two distinct types T1 and T2. The reasonable expectation is that all references that are considered equivalent should resolve to a single type definition instance.

If you are seeing something else, you might have run across a bug.

Mar 11, 2011 at 5:54 PM

I am not familiar with all the term 'type alias' covers, but it sounds like it may apply here in the presence of assembly unification. I think I oversimplified my statement of it, it's not two refs vs two defs but 1 ref 1 def vs 2 defs (well, '1 ref 1 ref-that-happens-to-be-a-def' in some cases). I can understand why a 'reference to this class in mscorlib 2.0.0.0' may be considered inequal to 'the definition of this class in mscorlib 4.0.0.0', but it is still rather inconvenient.

I'm running into this problem all over the place, but the one case I was able to consistently catch it was in my code is checking if a given method definition overrides a method on System.Object.

A simplified version looks like this:

 

public static bool OverridesObjectMethod(this IMethodDefinition method) {
    var obcm = MemberHelper.GetImplicitlyOverriddenBaseClassMethod(method);
    ITypeDefinition td = obcm.ContainingTypeDefinition;
    ITypeReference obj = td.PlatformType.SystemObject.ResolvedType;
    return TypeHelper.TypesAreEquivalent(td, obj);
}

 

If 'obj' does not do the resolution at the end, then this returns false for some cases, whereas as written it returns true.

What would you recommend in this case? In my example it's obvious that one is a def and one is a ref, but there seems to be many places in CCI where an ITypeReference is returned but it may in fact be a resolved definition. So is the only safe course to always use ResolvedType?

Coordinator
Mar 11, 2011 at 10:09 PM

In general, type references have so little information in them that any application that does non trivial analysis would be well advised to always resolve references.

Mar 14, 2011 at 4:39 PM

Sounds good, I will do this. Thanks for the info!

Mar 15, 2011 at 6:30 PM

I am overriding UnifyAssembly in my own host class to get over this:

public override AssemblyIdentity UnifyAssembly(AssemblyIdentity assemblyIdentity)
{
    var result = base.UnifyAssembly(assemblyIdentity);
    if (result != assemblyIdentity) return result;

    lock (GlobalLock.LockingObject)
    {
        result = null;
        foreach (var loadedUnit in LoadedUnits)
        {
            foreach (var unitReference in loadedUnit.UnitReferences)
            {
                if (unitReference.UnitIdentity.Name.UniqueKeyIgnoringCase != assemblyIdentity.Name.UniqueKeyIgnoringCase) continue;
                var existingAssemblyIdentity = unitReference.UnitIdentity as AssemblyIdentity;
                if ((existingAssemblyIdentity != null) &&
                    (existingAssemblyIdentity.Culture == assemblyIdentity.Culture) &&
                    IteratorHelper.EnumerablesAreEqual(existingAssemblyIdentity.PublicKeyToken, assemblyIdentity.PublicKeyToken) &&
                    ((result == null) || (result.Version < existingAssemblyIdentity.Version)))
                {
                    result = existingAssemblyIdentity;
                }
            }
        }
    }

    return result ?? assemblyIdentity;
} 

Although it works, is this the right thing to do?

Coordinator
Mar 15, 2011 at 8:05 PM

Unfortunately, there is no "right thing to do". If there were, the default host would do it.

This works only under the assumption that assemblies never break backwards compatibility. It also does not reflect the exact behavior of the CLR, which has also evolved over versions.

You really have to have good long hard look at your scenario and decide what behavior makes the most sense for it.

Apr 27, 2011 at 11:19 AM

I have a similar problem. I'm using MemberHelper.GetImplicitlyOverriddenBaseClassMethod and in some cases it doesn't work as expected. It uses the ParameterInformationComparer to compare the signatures of methods, which in turn calls TypeHelper.TypesAreEquivalent passing two ITypeReferences. Sometimes the InternedKeys of these two references are different even though the InternedKeys of the resolved types are the same (and the same as one of the references').

In other words: I have two references r1, r2 with different keys k1, k2. If I look at the resolved types, they both have k1 as InternedKeys.

I think that this is a bug and should not be possible at all. But if it is, shouldn't TypesAreEquivalent compare the keys of the resolved types instead of the references'?

Regards,
Daniel

Coordinator
Apr 27, 2011 at 2:26 PM

In general, it is possible for two different references to resolve to the same type. Either because of type forwarding or because of assembly unification.

Perhaps TypesAreEquivalent should be split into TypeReferencesAreEquivalent and TypesAreEquivalent.

This would be a breaking change. How does the community feel about such a change.

Apr 27, 2011 at 2:42 PM

Theoretically, assembly unification should not cause this, because existing implementation of IInternFactory uses UnifiedAssemblyIdentity when interning an unit reference (see InternFactory.GetUnitRootNamespaceInternId).

Does this mean that only type forwarding is the problem? I don't think so.

Coordinator
Apr 27, 2011 at 2:58 PM

There may well be bugs. If someone can come up with a small example that clearly does the wrong thing, that would be extremely helpful.

Apr 27, 2011 at 6:53 PM

Daniel or Jacob, have you tried my override of UnifyAssembly?

If it works, then I think the problem is in assembly unification, i.e. if you have assembly A referencing both, System.dll v4 and assembly B, and assembly B referencing System.dll v2, then the MetadataHostEnvironment will correctly unify the references only for mscorlib.dll, but not for System.dll. We then can come with a better implementation of this. And yes, I have another one.

Apr 28, 2011 at 10:49 AM

Unfortunately I already need to use my own implementation of UnifyAssembly and I have yet to figure out when exactly the problem happens.

Aug 3, 2011 at 12:28 PM

We have problems to detect overridden methods in assemblies which have been redirected using an application configuration <bindingRedirect> entry.

Does CCI currently support binding redirects?

If YES -> How to correctly add the application configuration to CCI?

If NO -> Is there a way to add a callback method so we can do this ourselves?

Best regards,
D.R.

 

Coordinator
Aug 3, 2011 at 2:33 PM

The default host does not know about application configuration files. If you add such support to your own host, you can override the ProbeAssemblyReference method to incorporate the information in the config file into the search.

Aug 5, 2011 at 8:48 AM

Thank you herman. We decided to go with artr's code above and ignore versioning at all for the moment.

 

BTW: We had to fix artr's code. The resolved type of {System.String[]}.ElementType has been a dummy for some reason => mscorlib hasn't been loaded in some cases. Problem is: If assemblyIdentity is already the CoreAssemblySymbolicIdentity then the comparison in line 2 fails to do its job. We replaced it with:

if (result == CoreAssemblySymbolicIdentity) return result;

and now it works like a charm.

 

Best regards,

D. R.

Aug 5, 2011 at 1:46 PM

I have changed the code since then. But your fix is helpful, nevertheless.

 

I now proble for assemblies in the directory of core assembly:

public override AssemblyIdentity UnifyAssembly(AssemblyIdentity assemblyIdentity)
{
    var result = base.UnifyAssembly(assemblyIdentity);
    if (result == CoreAssemblySymbolicIdentity) return result;

    lock (GlobalLock.LockingObject)
    {
        if (unifiedIdentities.TryGetValue(assemblyIdentity, out result)) return result;
    }

    if ((CoreAssemblySymbolicIdentity.Location.Length > 0) && IteratorHelper.EnumerableIsNotEmpty(assemblyIdentity.PublicKeyToken))
    {
        var coreAssemblyPath = Path.GetDirectoryName(CoreAssemblySymbolicIdentity.Location);
        var assemblyPath = Probe(coreAssemblyPath, assemblyIdentity.Name.Value);
        if (assemblyPath != null)
        {
            var assembly = LoadUnitFrom(assemblyPath) as IAssembly;
            if ((assembly != null) && !(assembly is Dummy))
            {
                lock (GlobalLock.LockingObject)
                {
                    unifiedIdentities.Add(assemblyIdentity, assembly.AssemblyIdentity);
                }
                return assembly.AssemblyIdentity;
            }
        }
    }
    return assemblyIdentity;
}

I think this mimics the way the CLR does the loading, i.e. if mscorlib.dll 4.0 is loaded, it will load System.Windows.Forms.dll v4, even if the reference is for 2.0. Isn't it?

Sep 13, 2011 at 12:32 PM
Edited Sep 13, 2011 at 12:33 PM

@artr: We recently run into problems with silverlight assemblies (Edit: even with the default host!) and you pointed out (here: http://ccimetadata.codeplex.com/discussions/254926 ) that your code should work around that. Thank you for publishing it!

Two questions:

1) unifiedIdentities is a simple Dictionary<AssemblyIdentity,AssemblyIdentity> cache?

2) Would you give us the implementation of the Probe() method? What exactly are you doing in there?

Thanks in advance!

Best regards,

D.R.

Sep 14, 2011 at 10:37 AM

@rubicon_dominik: Pleased to hear that it helps you. Maybe there are even better solutions somewhere (hermanv?)

Answers:

1) Yes, a simple caching dictionary.

2) Take a look at the private Probe() method, somewhere in one of the base host classes. I think I have simply copied it to my own host. Again, hermanv, maybe you can make it public?

Sep 14, 2011 at 10:55 AM
Edited Sep 14, 2011 at 10:58 AM

Thank you, I'll try your code and report back!

 

Best regards,

D.R.

Sep 14, 2011 at 11:11 AM

Report: There are still problems if we analyze Silverlight and non-Silverlight assemblies with a single CustomHost instance.

Even if we only analyze our Silverlight.DLL and nothing else we are facing problems with DummyDefinitions of system classes, for example:

DependDB.Analyzer.AnalyzerResolveException: Analyzing error when resolving 'System.Boolean System.Double.IsNaN(System.Double)'

So unfortunately your code doesn't seem to work for Silverlight too : (

Coordinator
Sep 14, 2011 at 5:37 PM

Art: The Probe method on MetadataHostEnvironment had no business being private. It is now virtual and protected.

rubicon_dominik: Check that the host has the right idea of what mscorlib is and that it knows how to find the Silverlight binaries.

Sep 14, 2011 at 6:52 PM

Thank you @hermanv, but my Probe() method actually looks a bit different:

private static string Probe(string probeDirectory, string name)
{
    var path = Path.Combine(probeDirectory, name + ".dll");
    if (File.Exists(path)) return path;
    path = Path.Combine(probeDirectory, name + ".exe");
    if (File.Exists(path)) return path;
    return null;
}

@rubicon_dominik, is Silverlight scenario still not working with my above UnifyAssembly and this Probe() method?

Have you added the path to Silverlight's reference assemblies to the LibPaths?