Member Cloning

Note

The documentation has a new home: Check it out!

Processing a .NET module often involves injecting additional code. Even though all models representing .NET metadata and CIL code are mutable, it might be very time-consuming and error-prone to manually import and inject metadata members and/or CIL code into the target module.

To help developers in injecting existing code into a module, AsmResolver.DotNet comes with a feature that involves cloning metadata members from one module and copying it over to another. All relevant classes are in the AsmResolver.DotNet.Cloning namespace:

using AsmResolver.DotNet.Cloning;

The MemberCloner class

The MemberCloner is the root object responsible for cloning members in a .NET module, and importing them into another.

In the snippet below, we define a new MemberCloner that is able to clone and import members into the module destinationModule:.

ModuleDefinition destinationModule = ...
var cloner = new MemberCloner(destinationModule);

In the remaining sections of this article, we assume that the MemberCloner is initialized using the code above.

Include members

The general idea of the MemberCloner is to first provide all the members to be cloned, and then clone everything all in one go. This is to allow the MemberCloner to fix up any cross references to members within the to-be-cloned metadata and CIL code.

For the sake of the example, we assume that the following two classes are to be injected in destinationModule:

public class Rectangle
{
    public Rectangle(Vector2 location, Vector2 size)
    {
        Location = location;
        Size = size;
    }

    public Vector2 Location { get; set; }
    public Vector2 Size { get; set; }

    public Vector2 GetCenter() => new Vector2(Location.X + Size.X / 2, Location.Y + Size.Y / 2);
}

public class Vector2
{
    public Vector2(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; set; }
    public int Y { get; set; }
}

The first step in cloning involves loading the source module, and finding the type definitions that correspond to these classes:

var sourceModule = ModuleDefinition.FromFile(...);
var rectangleType = sourceModule.TopLevelTypes.First(t => t.Name == "Rectangle");
var vectorType = sourceModule.TopLevelTypes.First(t => t.Name == "Vector2");

Alternatively, if the source assembly is loaded by the CLR, we also can look up the members by metadata token.

var sourceModule = ModuleDefinition.FromFile(typeof(Rectangle).Assembly.Location);
var rectangleType = (TypeDefinition) sourceModule.LookupMember(typeof(Rectangle).MetadataToken);
var vectorType = (TypeDefinition) sourceModule.LookupMember(typeof(Vector2).MetadataToken);

We can then use MemberCloner.Include to include the types in the cloning procedure:

cloner.Include(rectangleType, recursive: true);
cloner.Include(vectorType, recursive: true);

The recursive parameter indicates whether all members and nested types need to be included as well. This value is true by default and can also be omitted.

cloner.Include(rectangleType);
cloner.Include(vectorType);

Include returns the same MemberCloner instance. It is therefore also possible to create a long method chain of members to include in the cloning process.

cloner
    .Include(rectangleType)
    .Include(vectorType);

Cloning individual methods, fields, properties and/or events is also supported. This can be done by including the corresponding MethodDefinition, FieldDefinition, PropertyDefinition and/or EventDefinition instead.

Cloning the included members

When all members are included, it is possible to call MemberCloner.Clone to clone them all in one go.

var result = cloner.Clone();

The MemberCloner will automatically resolve any cross-references between types, fields and methods that are included in the cloning process.

For instance, going with the example in the previous section, if both the Rectangle as well as the Vector2 classes are included, any reference in Rectangle to Vector2 will be replaced with a reference to the cloned Vector2. If not all members are included, the MemberCloner will assume that these are references to external libraries, and will use the ReferenceImporter to construct references to these members instead.

Custom reference importers

The MemberCloner heavily depends on the CloneContextAwareReferenceImporter class for copying references into the destination module. This class is derived from ReferenceImporter, which has some limitations. In particular, limitations arise when cloning from modules targeting different framework versions, or when trying to reference members that may already exist in the target module (e.g., when dealing with NullableAttribute annotated metadata).

To account for these situations, the cloner allows for specifying custom reference importer instances. By deriving from the CloneContextAwareReferenceImporter class and overriding methods such as ImportMethod, we can reroute specific member references to the appropriate metadata if needed. Below is an example of a basic implementation of an importer that attempts to map method references from the System.Runtime.CompilerServices namespace to definitions that are already present in the target module:

public class MyImporter : CloneContextAwareReferenceImporter
{
    private static readonly SignatureComparer Comparer = new();

    public MyImporter(MemberCloneContext context)
        : base(context)
    {
    }

    public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method)
    {
        // Check if the method is from a type defined in the System.Runtime.CompilerServices namespace.
        if (method.DeclaringType is { Namespace.Value: "System.Runtime.CompilerServices" } type)
        {
            // We might already have a type and method defined in the target module (e.g., NullableAttribute::.ctor(int32)).
            // Try find it in the target module.

            var existingMethod = this.Context.Module
                .TopLevelTypes.FirstOrDefault(t => t.IsTypeOf(type.Namespace, type.Name))?
                .Methods.FirstOrDefault(m => method.Name == m.Name && Comparer.Equals(m.Signature, method.Signature));

            // If we found a matching definition, then return it instead of importing the reference.
            if (existingMethod is not null)
                return existingMethod;
        }

        return base.ImportMethod(method);
    }
}

We can then pass a custom importer factory to our member cloner constructor as follows:

var cloner = new MemberCloner(destinationModule, context => new MyImporter(context));

All references to methods defined in the System.Runtime.CompilerServices namespace will then be mapped to the appropriate method definitions if they exist in the target module.

See Common Caveats using the Importer for more information on reference importing and its caveats.

Post-processing of cloned members

In some cases, cloned members may need to be post-processed before they are injected into the target module. The MemberCloner class can be initialized with an instance of a IMemberClonerListener, that gets notified by the cloner object every time a definition was cloned.

Below is an example that appends the string _Cloned to the name for every cloned type.

public class MyListener : MemberClonerListener
{
    public override void OnClonedType(TypeDefinition original, TypeDefinition cloned)
    {
        cloned.Name = $"{original.Name}_Cloned";
        base.OnClonedType(original, cloned);
    }
}

We can then initialize our cloner with an instance of our listener class:

var cloner = new MemberCloner(destinationModule, new MyListener());

Alternatively, we can also override the more generic OnClonedMember instead, which gets fired for every member definition that was cloned.

public class MyListener : MemberClonerListener
{
    public override void OnClonedMember(IMemberDefinition original, IMemberDefinition cloned)
    {
        /* ... Do post processing here ... */
        base.OnClonedMember(original, cloned);
    }
}

As a shortcut, this can also be done by passing in a delegate or lambda instead to the MemberCloner constructor.

var cloner = new MemberCloner(destinationModule, (original, cloned) => {
    /* ... Do post processing here ... */
});

Injecting the cloned members

The Clone method returns a MemberCloneResult, which contains a register of all members cloned by the member cloner.

  • OriginalMembers: The collection containing all original members.
  • ClonedMembers: The collection containing all cloned members.
  • ClonedTopLevelTypes: A subset of ClonedMembers, containing all cloned top-level types.

Original members can be mapped to their cloned counterpart, using the GetClonedMember method:

var clonedRectangleType = result.GetClonedMember(rectangleType);

Alternatively, we can get all cloned top-level types.

var clonedTypes = result.ClonedTopLevelTypes;

It is important to note that the MemberCloner class itself does not inject any of the cloned members by itself. To inject the cloned types, we can for instance add them to the ModuleDefinition.TopLevelTypes collection:

foreach (var clonedType in clonedTypes)
    destinationModule.TopLevelTypes.Add(clonedType);

However, since injecting the cloned top level types is a very common use-case for the cloner, AsmResolver defines the InjectTypeClonerListener class that implements a cloner listener that injects all top-level types automatically into the destination module. In such a case, the code can be reduced to the following:

new MemberCloner(destinationModule, new InjectTypeClonerListener(destinationModule))
    .Include(rectangleType)
    .Include(vectorType)
    .Clone();

// `destinationModule` now contains copies of `rectangleType` and `vectorType`.