docs/design/features/typemap.md
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.
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
TypeMapAttribute assembly 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 the TypeMapAttribute constructor 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 TypeMapAssociationAttribute
to 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 TypeMapAssemblyTargetAttribute instances. This attribute would help the
Trimmer know other assemblies to examine for TypeMapAttribute and TypeMapAssociationAttribute
instances. 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 TypeMapAttribute a map from string to target Type.
(b) Using TypeMapAssociationAttribute a map from Type to Type (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, andTypeMapAssemblyTargetAttributeattributes are present in the final image after a trimming tool has been run.
TypeMapping.GetOrCreateExternalTypeMapping<> and
TypeMapping.GetOrCreateProxyTypeMapping<> as intrinsics (for example, Java via JavaTypeMapGroup). 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:
ldtoken IL instruction.unbox IL instruction.unbox.any IL instruction.isinst IL instruction.castclass IL instruction.box instruction.
box instruction and any corresponding unbox or unbox.any instructions.mkrefany instruction.refanyval instruction.newarr instruction.newobj instruction if it is a class type.call or ldftn, or the owning type of any method argument to callvirt or ldvirtftn.
Activator.CreateInstance<T> method.Type.GetType with 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.
An entry in the Proxy Type Map is included when the "source type" is referenced in one of the following ways:
ldtoken IL instruction when DynamicallyAccessedMembersAttribute is specified with one of the flags that preserves constructors for the storage location.Type.GetType with a constant string representing the type name when DynamicallyAccessedMembersAttribute is specified with one of the flags that preserves constructors for the storage location.newobj instruction.Activator.CreateInstance<T> method.box instruction.newarr instruction.mkrefany instruction.refanyval instruction.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:
isinst IL instruction.castclass IL instruction.callvirt, or ldvirtftn.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.