Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
4c1143b
Track type cast targets
jkoritzinsky May 27, 2025
ce10c21
Scan for casts as part of the MethodBodyScanner instead
jkoritzinsky May 27, 2025
5495324
After feedback, first draft implementing external type map (compiler …
jkoritzinsky Jun 3, 2025
bb84f34
Add first pass of external type map implementation for NAOT runtime
jkoritzinsky Jun 3, 2025
650c033
Force the target type in the external type map entry to be constructa…
jkoritzinsky Jun 3, 2025
1e9b608
First pass implementing the proxy type map
jkoritzinsky Jun 4, 2025
a619e5d
Fix compiler asserts
jkoritzinsky Jun 4, 2025
2a5531e
Don't root type map custom attributes
jkoritzinsky Jun 4, 2025
c7f3638
Provide a mechanism to specify NativeAOT-only test apps
jkoritzinsky Jun 4, 2025
847760b
Fix format writing so it actually works
jkoritzinsky Jun 4, 2025
6c825d8
Cache type map attribute type lookup
jkoritzinsky Jun 5, 2025
64af9d3
Add additional node in the IL Scanner when we see a cast that may be …
jkoritzinsky Jun 5, 2025
c3bf1ed
Enable the TypeMapApp test for NativeAOT and adjust as necessary to m…
jkoritzinsky Jun 5, 2025
2ed2d97
Split type map manager type so we don't need to pass null
jkoritzinsky Jun 5, 2025
4334a63
Match attributes based on name. Don't care about the assembly
jkoritzinsky Jun 5, 2025
654731b
Hook up the type map manager to the trimming test driver
jkoritzinsky Jun 5, 2025
89a776f
Add TrimmingTest and add one more test case in the NativeAOT test app
jkoritzinsky Jun 5, 2025
99cb5c9
Add warning validation
jkoritzinsky Jun 5, 2025
b73848d
Fix node name
jkoritzinsky Jun 5, 2025
0f083a5
Don't materialize a string for every entry as we search the hashtable
jkoritzinsky Jun 5, 2025
a48496a
Remove the "type map entry" nodes and rename the "associated" type ma…
jkoritzinsky Jun 6, 2025
bcf98ef
Collapse MetadataBasedTypeMapManager and TypeMapManager
jkoritzinsky Jun 6, 2025
462334c
We know all the type map nodes up front in the AnalysisBasedTypeMapMa…
jkoritzinsky Jun 6, 2025
e4ff1d9
Add an interface for each set of nodes to collapse the number of list…
jkoritzinsky Jun 6, 2025
1881042
Refactor the concept of "optimized out observations we must not forge…
jkoritzinsky Jun 6, 2025
3a9dbc9
Move attribute check outside of UsageBasedMetadataManager
jkoritzinsky Jun 6, 2025
098f57f
Put TypeMapStates into its own file as a top-level type
jkoritzinsky Jun 6, 2025
b3026cb
Fix comparison to match pattern
jkoritzinsky Jun 6, 2025
de0ed0b
Fix cast typo
jkoritzinsky Jun 7, 2025
2f40f9c
Make diagnostic names for invalid type map nodes unique
jkoritzinsky Jun 7, 2025
0445c4c
Just use reference equality and the default hash code for the hashtab…
jkoritzinsky Jun 9, 2025
3eae3d1
Add some more test cases around interfaces and use nameof instead of …
jkoritzinsky Jun 9, 2025
46cd22b
Add support in the illink analyzer test host for no-body classes and …
jkoritzinsky Jun 9, 2025
1659b1e
Don't try to remember optimizations. Just root whatever we scanned du…
jkoritzinsky Jun 10, 2025
0b037e8
Add comment on TrimTarget
jkoritzinsky Jun 10, 2025
aa1ada7
Fix failures in TypeMapApp test
jkoritzinsky Jun 11, 2025
da59332
Seal types
jkoritzinsky Jun 12, 2025
c37d585
Put stubs on the generated asssembly
jkoritzinsky Jun 12, 2025
44c0635
Don't instantiate Dictionary<TKey, TValue> on the "no map" path.
jkoritzinsky Jun 12, 2025
680e8bc
Persist maps for all requests and throw an exception when no map is a…
jkoritzinsky Jun 12, 2025
a31d9dc
Fix indentation and revert change in NativeFormatWriter
jkoritzinsky Jun 13, 2025
74c8a5a
Add more interesting test cases.
jkoritzinsky Jun 16, 2025
364edc7
Merge branch 'main' of https://github.com/dotnet/runtime into typemap…
jkoritzinsky Jun 17, 2025
1571bf5
Add a more in-depth spec at the IL level describing the type map feature
jkoritzinsky Jun 18, 2025
41a894b
PR feedback
jkoritzinsky Jun 18, 2025
e45a026
Cleanup based on primary constructor guidance
jkoritzinsky Jun 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions docs/design/features/typemap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Interop Type Map

## Background

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](https://github.com/microsoft/CsWinRT/b1733e95c6d35b551fc8cf6fe04e2a0c287346dd/master/src/WinRT.Runtime/Projections.CustomTypeMappings.tt), [Generation of vtables for AOT](https://github.com/microsoft/CsWinRT/blob/b1733e95c6d35b551fc8cf6fe04e2a0c287346dd/src/Authoring/WinRT.SourceGenerator/AotOptimizer.cs#L1597).

* .NET For Android - [Assembly Store doc](https://github.com/dotnet/android/blob/b8d0669e951d683443c19ecac06dc96363791820/Documentation/project-docs/AssemblyStores.md), [Assembly Store generator](https://github.com/dotnet/android/blob/b8d0669e951d683443c19ecac06dc96363791820/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingReleaseNativeAssemblyGenerator.cs), [unmanaged Assembly Store types](https://github.com/dotnet/android/blob/b8d0669e951d683443c19ecac06dc96363791820/src/native/xamarin-app-stub/xamarin-app.hh).

* Objective-C - [Registrar](https://github.com/dotnet/macios/blob/cee75657955e29981ded2fb0c6f0ee832db9a8d3/src/ObjCRuntime/Registrar.cs#L87), [Managed Static Registrar](https://github.com/dotnet/macios/blob/cee75657955e29981ded2fb0c6f0ee832db9a8d3/docs/managed-static-registrar.md).

## Priorties

1) Trimmer friendly - AOT compatible.
2) Usable from both managed and unmanaged environments.
3) Low impact to application start-up and/or Assembly load.
4) Be composable - handle multiple type mappings.

## APIs

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)**
```csharp
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.

1. 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 "target type" will be treated as the "trim-target".

2. 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.

3. 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.

4. 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`, and `TypeMapAssemblyTargetAttribute` attributes are present in the final image after a trimming tool has been run.


5. Trimming tools will consider calls to `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).

## Type Map entry trimming rules

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.

### Type Map Assembly Target probing

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 `TypeMapAssemblyTargetAttribute`s 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.

### External Type Map

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 `ldtoken` IL instruction.
- The argument to the `unbox` IL instruction.
- The argument to the `unbox.any` IL instruction.
- The argument to the `isinst` IL instruction.
- The argument to the `castclass` IL instruction.
- The argument to the `box` instruction.
- The argument to the `mkrefany` instruction.
- The argument to the `refanyval` instruction.
- The argument to the `newarr` instruction.
- The argument to the `ldobj` instruction.
- The argument to the `stobj` instruction.
- The argument to the `.constrained` instruction prefix.
- The type of a method argument to the `newobj` instruction.
- The owning type of the method argument to `call`, `callvirt`, `ldftn`, or `ldvirtftn`.
- 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.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.

### Proxy Type Map

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 `ldtoken` IL instruction when `DynamicallyAccessedMembersAttribute` is specified with one of the flags that preserves constructors for the storage location.
- Calls to `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.
- The type of a method argument to the `newobj` instruction.
- The generic argument to the `Activator.CreateInstance<T>` method.
- The argument to the `box` instruction.
- The argument to the `newarr` instruction.
- The argument to the `.constrained` instruction prefix.
- The argument to the `mkrefany` instruction.
- The argument to the `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:

- The argument to the `isinst` IL instruction.
- The argument to the `castclass` IL instruction.
- The owning type of the method argument to `callvirt`, or `ldvirtftn`.
3 changes: 2 additions & 1 deletion eng/testing/linker/trimmingTests.targets
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<_SkippedAppSourceFiles Include="@(TestConsoleAppSourceFiles)" Condition="$([System.String]::Copy('%(TestConsoleAppSourceFiles.SkipOnTestRuntimes)').Contains('$(TargetRid)'))" />

<_SkippedAppSourceFiles Include="@(TestConsoleAppSourceFiles)" Condition="'$(RunNativeAotTestApps)' == 'true' and '%(TestConsoleAppSourceFiles.NativeAotIncompatible)' == 'true'" />
<_SkippedAppSourceFiles Include="@(TestConsoleAppSourceFiles)" Condition="'$(RunNativeAotTestApps)' != 'true' and '%(TestConsoleAppSourceFiles.NativeAotOnly)' == 'true'" />

<_AppSourceFiles Include="@(TestConsoleAppSourceFiles)" Exclude="@(_SkippedAppSourceFiles)" />

Expand Down Expand Up @@ -165,7 +166,7 @@
</Exec>

<Error Condition="'$(ExecutionExitCode)' != '100'" Text="Error: [Failed Test]: %(TestConsoleApps.ProjectCompileItems). The Command %(TestConsoleApps.TestCommand) return a non-success exit code $(ExecutionExitCode)." ContinueOnError="ErrorAndContinue" />

<!-- Remove test projects dir to save disk space if the test was successful. Ignore failures as this is best effort.
Don't use Removedir as ContinueOnError on it doesn't work when using warnaserror/TreatWarningsAsErrors. -->
<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,4 +825,8 @@
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Diagnostics.DiagnosticMethodInfo.#ctor(System.String,System.String,System.String)</Target>
</Suppression>
</Suppressions>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Internal.NativeFormat.TypeHashingAlgorithms</Target>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
<Compile Include="System\Runtime\CompilerServices\StaticClassConstructionContext.cs" />
<Compile Include="System\Runtime\ExceptionIDs.cs" />
<Compile Include="System\Runtime\GCSettings.NativeAot.cs" />
<Compile Include="System\Runtime\InteropServices\TypeMapLazyDictionary.NativeAot.cs" />
<Compile Include="System\Runtime\TypeLoaderExports.cs" />
<Compile Include="System\Runtime\ThunkPool.cs" />
<Compile Include="System\Runtime\InteropServices\ComEventsHelper.NativeAot.cs" Condition="'$(FeatureCominterop)' == 'true'" />
Expand Down Expand Up @@ -322,6 +323,9 @@
<Compile Include="$(CompilerCommonPath)\TypeSystem\Common\Utilities\LockFreeReaderHashtableOfPointers.cs">
<Link>Utilities\LockFreeReaderHashtableOfPointers.cs</Link>
</Compile>
<Compile Include="$(CompilerCommonPath)\TypeSystem\Common\TypeHashingAlgorithms.cs">
<Link>Utilities\TypeHashingAlgorithms.cs</Link>
</Compile>
<Compile Include="$(AotCommonPath)\System\Collections\Generic\LowLevelList.cs">
<Link>System\Collections\Generic\LowLevelList.cs</Link>
</Compile>
Expand Down
Loading
Loading