When interop between languages/platforms involves the projection of types, some kind of type mapping logic must often exist. This mapping mechanism is used to determine what .NET type should be used to project a type from language X and vice versa.
The most common mechanism for this is the generation of a large look-up table at build time, which is then injected into the application or Assembly. If injected into the Assembly, there is typically some registration mechanism for the mapping data. Additional modifications and optimizations can be applied based on the user experience or scenarios constraints (that is, build time, execution environment limitations, etc).
Prior to .NET 10 there were at least three (3) bespoke mechanisms for this in the .NET ecosystem:
-
C#/WinRT - Built-in mappings, Generation of vtables for AOT.
-
.NET For Android - Assembly Store doc, Assembly Store generator, unmanaged Assembly Store types.
-
Objective-C - Registrar, Managed Static Registrar.
- Trimmer friendly - AOT compatible.
- Usable from both managed and unmanaged environments.
- Low impact to application start-up and/or Assembly load.
- Be composable - handle multiple type mappings.
The below .NET APIs represents only part of the feature. The complete scenario would involve additional steps and tooling.
Provided by BCL (that is, NetCoreApp)
namespace System.Runtime.InteropServices;
/// <summary>
/// Type mapping between a string and a type.
/// </summary>
/// <typeparam name="TTypeMapGroup">Type universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class TypeMapAttribute<TTypeMapGroup> : Attribute
{
/// <summary>
/// Create a mapping between a value and a <see cref="System.Type"/>.
/// </summary>
/// <param name="value">String representation of key</param>
/// <param name="target">Type value</param>
/// <remarks>
/// This mapping is unconditionally inserted into the type map.
/// </remarks>
public TypeMapAttribute(string value, Type target)
{ }
/// <summary>
/// Create a mapping between a value and a <see cref="System.Type"/>.
/// </summary>
/// <param name="value">String representation of key</param>
/// <param name="target">Type value</param>
/// <param name="trimTarget">Type used by Trimmer to determine type map inclusion.</param>
/// <remarks>
/// This mapping is only included in the type map if the Trimmer observes a type check
/// using the <see cref="System.Type"/> represented by <paramref name="trimTarget"/>.
/// </remarks>
[RequiresUnreferencedCode("Interop types may be removed by trimming")]
public TypeMapAttribute(string value, Type target, Type trimTarget)
{ }
}
/// <summary>
/// Declare an assembly that should be inspected during type map building.
/// </summary>
/// <typeparam name="TTypeMapGroup">Type universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class TypeMapAssemblyTargetAttribute<TTypeMapGroup> : Attribute
{
/// <summary>
/// Provide the assembly to look for type mapping attributes.
/// </summary>
/// <param name="assemblyName">Assembly to reference</param>
public TypeMapAssemblyTargetAttribute(string assemblyName)
{ }
}
/// <summary>
/// Create a type association between a type and its proxy.
/// </summary>
/// <typeparam name="TTypeMapGroup">Type universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class TypeMapAssociationAttribute<TTypeMapGroup> : Attribute
{
/// <summary>
/// Create an association between two types in the type map.
/// </summary>
/// <param name="source">Target type.</param>
/// <param name="proxy">Type to associated with <paramref name="source"/>.</param>
/// <remarks>
/// This mapping will only exist in the type map if the Trimmer observes
/// an allocation using the <see cref="System.Type"/> represented by <paramref name="source"/>.
/// </remarks>
public TypeMapAssociationAttribute(Type source, Type proxy)
{ }
}
/// <summary>
/// Entry type for interop type mapping logic.
/// </summary>
public static class TypeMapping
{
/// <summary>
/// Returns the External type type map generated for the current application.
/// </summary>
/// <typeparam name="TTypeMapGroup">Type universe</typeparam>
/// <param name="map">Requested type map</param>
/// <returns>True if the map is returned, otherwise false.</returns>
/// <remarks>
/// Call sites are treated as an intrinsic by the Trimmer and implemented inline.
/// </remarks>
[RequiresUnreferencedCode("Interop types may be removed by trimming")]
public static IReadOnlyDictionary<string, Type> GetOrCreateExternalTypeMapping<TTypeMapGroup>();
/// <summary>
/// Returns the associated type type map generated for the current application.
/// </summary>
/// <typeparam name="TTypeMapGroup">Type universe</typeparam>
/// <param name="map">Requested type map</param>
/// <returns>True if the map is returned, otherwise false.</returns>
/// <remarks>
/// Call sites are treated as an intrinsic by the Trimmer and implemented inline.
/// </remarks>
[RequiresUnreferencedCode("Interop types may be removed by trimming")]
public static IReadOnlyDictionary<Type, Type> GetOrCreateProxyTypeMapping<TTypeMapGroup>();
}Given the above types the following would take place.
-
Types involved in unmanaged-to-managed interop operations would be referenced in a
TypeMapAttributeassembly attribute that declared the external type system name, a target type, and optionally a "trim-target" to determine if the target type should be included in the map. If theTypeMapAttributeconstructor that doesn't take a trim-target is used the entry will always be emitted into the type map. -
Types used in a managed-to-unmanaged interop operation would use
TypeMapAssociationAttributeto define a conditional link between the source and proxy type. In other words, if the source is kept, so is the proxy type. If the Trimmer observes an explicit allocation of the source type, the entry will be inserted into the map. -
During application build, source would be generated and injected into the application that defines appropriate
TypeMapAssemblyTargetAttributeinstances. This attribute would help the Trimmer know other assemblies to examine forTypeMapAttributeandTypeMapAssociationAttributeinstances. These linked assemblies could also be used in the non-Trimmed scenario whereby we avoid creating the map at build-time and create a dynamic map at run-time instead. -
The Trimmer will build two maps based on the above attributes from the application reference closure.
(a) Using
TypeMapAttributea map fromstringto targetType.(b) Using
TypeMapAssociationAttributea map fromTypetoType(source to proxy).
Important
Conflicting key/value mappings are not allowed.
Note
The underlying format of the produced maps is implementation-defined. Different .NET form factors may use different formats.
Additionally, it is not guaranteed that the TypeMapAttribute, TypeMapAssociationAttribute, and TypeMapAssemblyTargetAttribute attributes are present in the final image after a trimming tool has been run.
- Trimming tools will consider calls to
TypeMapping.GetOrCreateExternalTypeMapping<>andTypeMapping.GetOrCreateProxyTypeMapping<>as intrinsics (for example, Java viaJavaTypeMapGroup). As a result, it is not trim-compatible to call either of these methods with non-fully-instantiated generic (such as a type argument or a type that is instantiated over a type argument).
This section provides the minimum rules for entries to be included in a given type map by a trimming tool (ie. ILLink or NativeAOT). Due to restrictions in some form factors, some trimming tools may include more entries than would be included based on the rules described below.
The following rules only apply to code that is considered "reachable" from the entry-point method. Code that a trimming tool determines is unreachable does not contribute to determining if a type map entry is preserved.
The process of building type maps starts at the entry-point method of the app (the Main method). The initial entries for the type maps are collected from the assembly containing the entry-point for the app. From that assembly, any assembly names that are mentioned in a TypeMapAssemblyTargetAttribute are scanned. This process then repeats for those assemblies until all assemblies transitively referenced by TypeMapAssemblyTargetAttributes have been scanned.
An assembly name mentioned in the TypeMapAssemblyTargetAttribute does not need to map to an AssemblyRef row in the module's metadata. As long as a given name can be resolved by the runtime or by whatever trimming tool is run on the application, it can be used.
An entry in an External Type Map is included when the "trim target" type is referenced in one of the following ways:
- The argument to the
ldtokenIL instruction. - The argument to the
unboxIL instruction. - The argument to the
unbox.anyIL instruction. - The argument to the
isinstIL instruction. - The argument to the
castclassIL instruction. - The argument to the
boxinstruction.- If the trimming tool can determine that this box does not escape and could be stack allocated, it can ignore this
boxinstruction and any correspondingunboxorunbox.anyinstructions.
- If the trimming tool can determine that this box does not escape and could be stack allocated, it can ignore this
- The argument to the
mkrefanyinstruction. - The argument to the
refanyvalinstruction. - The argument to the
newarrinstruction. - The type of a method argument to the
newobjinstruction if it is a class type. - The owning type of an instance method argument to
callorldftn, or the owning type of any method argument tocallvirtorldvirtftn.- If the owning type is an interface and the trimming tool can determine that there is only one implementation of the interface, it is free to interpret the method token argument as though it is the method on the only implementing type.
- The generic argument to the
Activator.CreateInstance<T>method. - Calls to
Type.GetTypewith a constant string representing the type name.
Many of these instructions can be passed a generic parameter. In that case, the trimming tool should consider type arguments of instantiations of that type as having met one of these rules and include any entries with those types as "trim target" types.
For pointer, byref, and array types as trim targets, the entries are preserved if the element type meets the requirements above to keep the entry. A trimming tool is free to remove entries if it can prove that the pointer, byref, or array type cannot be represented at runtime.
An entry in the Proxy Type Map is included when the "source type" is referenced in one of the following ways:
- The argument to the
ldtokenIL instruction whenDynamicallyAccessedMembersAttributeis specified with one of the flags that preserves constructors for the storage location. - Calls to
Type.GetTypewith a constant string representing the type name whenDynamicallyAccessedMembersAttributeis specified with one of the flags that preserves constructors for the storage location. - The type of a method argument to the
newobjinstruction. - The generic argument to the
Activator.CreateInstance<T>method. - The argument to the
boxinstruction. - The argument to the
newarrinstruction. - The argument to the
mkrefanyinstruction. - The argument to the
refanyvalinstruction.
If the type is an interface type and the user could possibly see a RuntimeTypeHandle for the type as part of a casting or virtual method resolution operation (such as with System.Runtime.InteropServices.IDynamicInterfaceCastable), then the following cases also apply:
- The argument to the
isinstIL instruction. - The argument to the
castclassIL instruction. - The owning type of the method argument to
callvirt, orldvirtftn.
Finally, if the trimming tool determines that it is impossible to retrieve a System.Type instance the represents the "source type" at runtime, then the entry may be omitted from the Proxy Type Map as its existence is unobservable.
For pointer, byref, and array types as source types, the entries are preserved if the element type meets the requirements above to keep the entry. A trimming tool is free to remove entries if it can prove that the pointer, byref, or array type cannot be represented at runtime.