The Lightweight Metadata Reader (LMR) is an implementation of the Reflection APIs built on top of IMetaDataImport. It is implements the Reflection-Only subset (nothing that manipulates live objects, no Emit, no Dynamic methods) and is a more tooling-friendly implementation of reflection.

LMR provides alternative implementations of System.Type, MethodInfo and other reflection objects. These implementations are built entirely in C# and take absolutely no dependency on the CLR Loader.

LMR is useful if you want to use the existing Reflection-Only APIs in a tooling scenario. LMR has the following qualities for being tooling-friendly:

  1. Absolutely no dependency on the CLR Loader or host process.
  2. Ability to multi-target which version of the CLR is loaded (including multi-targeting Mscorlib).
  3. No global state, no dependencies on AppDomain flags or callbacks. Ability to load multiple versions of the same dll independently
  4. Complete control over assembly resolution. No eagerly resolving assembly references.
  5. Complete control over access to the file system, including file locks.
  6. Ability to unload all assemblies. After all, the assemblies are loaded as metadata files and so not subject to any restrictions imposed by the CLR loader.
  7. No automatic caching layer. Reflection has a caching layer that can bloat working set usage.

 

 

New LMR concepts

LMR keeps true to the reflection API, but also exposes additional interfaces on top of reflection to facilitate the above goals.

ITypeUniverse

The key new concept in LMR is an ITypeUniverse. An ITypeUniverse provides the unit of containment for inspecting assemblies.

internal interface ITypeUniverse
{
    Type GetBuiltInType(CorElementType elementType);
    Type GetTypeFromName(string fullName);
    Assembly GetSystemAssembly();
    Assembly ResolveAssembly(AssemblyName name);
    Assembly ResolveAssembly(Module scope, Token tokenAssemblyRef);
    Module ResolveModule(Assembly containingAssembly, string moduleName);
}

All  LMR assemblies are live in a specific ITypeUniverse. The key invariants around an ITypeUniverse are:

  1. Isolation: Universers are completely isolated from each other. They share nothing and contend over nothing. For example, two separate universes can each load their own separate versions of mscorlib, and have their own separate assembly resolution policies.
  2. Closed set: If a reflection object (eg, a type, methodInfo, etc) is in a given type universe, than any other reflection object obtained from it will be in the same universe.
  3. Disjoint: universes are totally disjoint. Each reflection objects lives in a single universe.

Conceptually, an AppDomain is like an ITypeUniverse for real reflection assemblies. (Modulo optimizations where some things are shared across appdomains). Practically this means that the System.Type for an Int loaded by LMR is not the same as typeof(int), because they’re in different type universes. Afterall, typeof(int) returns the int for the version of mscorlib that the host process is running against, whereas the LMR int may be from a totally different version of mscorlib.

These invariants are directly tied to the LMR tooling goals.

New optional Type interfaces for avoiding resolution

LMR straddles the impossible boundary of preserving the reflection API while also avoiding eager assembly resolution. The key way LMR does this is by returning objects that are proxies and lazily do assembly resolution.

For example, if you ask for a base type that’s actually a TypeRef (eg, would resolve to another assembly), Reflection would just eagerly resolve the type and force assembly resolution (and possible failure) at the Type.BaseType call. Whereas LMR returns a proxy System.Type implementation that just wraps the TypeRef and implements a new ITypeReference interface:

 

internal interface ITypeProxy
{
    // Get the Type that this resolves to. 
    // For a TypeRef, this may invoke the resolver callbacks, which could imply
    // arbitrary policy from resolving, to loading new assemblies into the TypeUniverse, to requesting
    // information from the User. This can also return a "dummy" type. 
    //
    // This should throw instead of returning null to indicate an error. 
    Type GetResolvedType();


    /// <summary>
    /// Get the type universe that this type is in. A type and its resolved type are in the same universe.
    /// </summary>
    ITypeUniverse TypeUniverse { get; }
}
internal interface ITypeReference : ITypeProxy
{
    Token TypeRefToken { get; }
    string RawName {get; }
    Token ResolutionScope { get; }
string FullName { get; } Module DeclaringScope { get; } }

Practically, this means that regular reflection code will still work, the assembly resolution would just be pushed to later.  But LMR aware code can explicitly query the System.Type returned from Type.BaseType for the ITypeReference and then use that interface to avoid resolution.

No Reference equality

Reflection maintains reference equality for reflection objects, especially for System.Type. Eg, Object.ReferenceEquals(typeof(Foo), typeof(Foo)) will always be true. LMR does not maintain such equality, although it still overrides Type operator= for comparison. Part of the LMR motivation is that:

  1. reference equality mandates a caching policy which can bloat working set.
  2. LMR uses “tearoff” objects frequently, and reference equality can hurt performance by preventing the tearoff pattern. Sometimes, just determining that two types are the same is more expensive than returning the tearoff objects.
  3. Reference equality forces eager assembly resolution to determine if two TypeRefs actually match to the same type. LMR avoids eager assembly resolution.
  4. Reference equality would prevent LMR’s pattern of having special interfaces (ITypeReference) for exposing the raw metadata token functionality.

 

Example usage:

Here’s an example usage showing using LMR to dump all the types in a single assembly, without loading any other assemblies. 

The DefaultUniverse class is 

// Simple example to just print type names. 
// Doesn't require any assembly resolution. Does not even load mscorlib.
static void PrintTypeNames(string filename)
{
    using (DefaultUniverse u = new DefaultUniverse())
    {
        Assembly a = u.LoadAssemblyFromFile(filename);

        Console.WriteLine("Opened assembly:{0}", filename);
        Console.WriteLine("   assemblyName={0}", a.GetName());
        Debug.Assert(u.ResolveAssembly(a.GetName()).Equals(a)); // sanity check.

        Console.WriteLine("Types in assembly:");
        foreach (Type t in a.GetTypes())
        {
            Console.WriteLine("  " + t.FullName);
        }
    }
    // Dispose called, it's unloaded
}

Last edited Aug 7, 2012 at 12:39 AM by MikeStall, version 4