How to copy a type from one assembly to another?

Topics: Metadata Model
Jun 20, 2009 at 1:32 PM
Edited Jun 20, 2009 at 2:34 PM

Assuming this type doesn't reference any types inside the assembly (only FCL types), what's needed to copy the type completely to another assembly?

At least, it's necessary

1) copy TypeDefinition to the target module

2) change ContainingUnitNamespace (how to do this correctly?)

3) anything else?

Coordinator
Jun 20, 2009 at 6:13 PM

In general, the only thing that binds a namespace type definition to its defining assembly/module is its ContainingUnitNamespace. Since the object model is immutable, the only way to change it is to make a copy. The usual way to make a copy is to use theMetadataMutator from the MutableMetadataModel project. Note that, not only must ContainingUnitNamespace be updated to point to the target namespace, but the target namespace must be updated to include the copy in its member list. Another wrinkle is that the flat list of types (AllTypes) of the target assembly must also be updated to include the copy. Making these changes to the receiving assembly means that you need to get hold of the mutable versions of the target namespace and assembly. One way to do this is to subclass the metadata mutator and then to override the methods that visits the mutable versions of namespace and assembly. The overrides can then make the necessary modifications, along with visiting the the children (which can be done by invoking the base class method).

Of course, if you want to move the type from one assembly to another, rather than just copy it, you'll have to make a copy of the other assembly that has been modified (mutated) to omit the type.

The fun part starts when the type you want to copy or move contains references to other types. For the most part it is not a problem, the copy just brings its references with it and the PEWriter will take care of the details of creating the necessary entries in the metadata tables. However, if a type references itself, the copy of the type must modify not just its ContainingUnitNamespace, but all type references that point to the type itself. Not suprisingly, the mutator has mechanisms to deal with this. If you look at Visit(INamespaceTypeDefinition), you'll see that the copy it creates is placed in a dictionary with the orginal as its key. This dictionary (stored in the cache field of the mutator) is consulted whenever a type reference is visited and if the reference is to a type that has been copied, then the reference is updated to reference the copy instead. This works even when you copy/move several types.

However, if the type you are copying contains references to internal types of the original assembly, the receiving assembly will have to be included in the list of "friend" assemblies of the original assembly. Alternatively, those types have to be marked as public and any members of those types that are referenced have to be marked as public. Likewise for base classes, interfaces and so on. Life can get complicated.

Moving is even more fun, since the types have to be deleted from the original assembly and any references that remain in the original assembly will have to be redirected to point to the receiving assembly. You can do that by redirecting the cache of the mutator of the original assembly to point to the receiving assembly. This will only fix up references that are inside the original assembly, however. If there are other assemblies that reference the moved type, you either have to fix up all of those assemblies too, or you have to create a type alias ("type forwarder") and put it in the ExportedTypes collection of the rewritten assembly.

Jun 20, 2009 at 6:20 PM

Thanks very much. One other question. What's the simplest way to create target's nested namespace? (assuming I wanna to keep classes' namespace the same)

Coordinator
Jun 20, 2009 at 6:41 PM

If the target assembly does not already have the right namespace to contain the type that you are copying, you'll have to create them. Arranging to do this can be a bit tricky. One way that comes to mind is to first make a copy of the receiving assembly using a standard mutator. Then search this assembly for the namespace that contains the type being copied. If no such namespace is found, then construct a mutable nested namespace and insert the copied type into it. Then search the assembly for the namespace that contains the nested namespace and insert the newly copied nested namespace into it. Do this recursively (the root namespace will stop the recursion, at the last resort). It would be most efficient to combine the search with the creation of copies. I.e. you'd might want to write a method with a signature similar to "MutableNestedNamespace FindOrCreateContainingNamespace(IAssembly targetAssembly, INestedNamespace sourceNamespace)".

Jun 29, 2009 at 12:22 PM
Edited Jun 29, 2009 at 12:23 PM

And one other thing; how to copy generic types correctly?

When I just cast it to INamespaceTypeDefinition ang get copy of it with mutator, peverify shows errors like these:

[MD]: Error: Signature has generic type of arity 0 instantiated at different arity 1 at byte=0x00000006. [token:0x11000001]
[token 0x0200000A] Type load failed.

 

My test class is very simple:

 public class GenericClass<T>
 {
 }

Coordinator
Jun 29, 2009 at 4:22 PM

You should not have to do anything special to copy generic types. The error message is not much to go on. It is complaining that a reference to a generic type found in a local variable signature of a method is to a non generic type. The type in question is the tenth type in the type def table, so you seem to have more than just two types in your target assembly.

Can you supply more details or a small test program that reproduces the unexpected behavior?

Jun 29, 2009 at 5:32 PM
Edited Jun 29, 2009 at 5:52 PM
Well, I made a very small test with the only generic type and consumer type, and got the same errors. 
When I removed reference to the generic type from the consumer type, I'm still getting this error: 
[token  0x02000003] Type load failed.
And when I inspected generated assembly with ildasm, I found that generic type really was transformed 
to non-generic type. So, could you give a hint what's wrong in the code below?
It works fine with non-generic types. I can make a repro-pack if this code is not enough.

Module _target;

		void Append(IModule source)
		{
			foreach (var type in source.GetAllTypes())
			{
				if (type.Name.ToString() == ModuleTypeName)
					continue;
				TypeDefinition newType = CloneType(type);
				_target.AllTypes.Add(newType);
			}
		}

		TypeDefinition CloneType(INamedTypeDefinition type)
		{
			if (type is INamespaceTypeDefinition)
				return CloneType((INamespaceTypeDefinition) type);

			throw new InternalErrorException();
		}

		TypeDefinition CloneType(INamespaceTypeDefinition type)
		{
			var res = _mutator.GetMutableCopy(type);
			var ns = EnsureNestedNamespaceExists(res.ContainingUnitNamespace);
			res.ContainingUnitNamespace = ns;
			ns.Members.Add(res);

			return res;
		}
Coordinator
Jun 29, 2009 at 5:40 PM

When you clone the type you should also visit it in the mutator. GetMutableCopy makes a shallow copy, which is not good enough.

Jun 29, 2009 at 5:58 PM
Edited Jun 30, 2009 at 3:29 AM

How to do this correctly? I made it in this way, but "type load" error still remains:

		TypeDefinition CloneType(INamespaceTypeDefinition type)
		{
			var res = _mutator.GetMutableCopy(type);
			res = _mutator.Visit(res);
			var ns = EnsureNestedNamespaceExists(type.ContainingUnitNamespace);
			res.ContainingUnitNamespace = ns;
			ns.Members.Add(res);
			return res;
		}

Jun 30, 2009 at 11:26 AM
Edited Jun 30, 2009 at 11:27 AM

I've found the reason, it was module props. TargetRuntimeVersion was set to v2.0, but MetadataFormat was set to 1.0 and so PeWriter produced ill-formed file.