Description
Background and motivation
The TypeMap is a solution for large interop based frameworks (for example, Java, Swift, WinRT, etc). The issue is attempting to map a type from another platform to the .NET platform and vice versa. The classic COM scenario uses the Guid
type as the common key. Note that all .NET types have an associated GUID
and therefore one can easily map from Type
to Guid
with minimal effort. There are two interop scenarios to consider the reverse P/Invoke (unmanaged to managed) or P/Invoke (managed to unmanaged).
For the reverse P/Invoke some type from another environment is entering the managed environment, what type do we map this instance to?
Examples: Guid
-> Type
(COM) or string
-> Type
(WinRT)
The P/Invoke means a Type
is leaving managed code and being projected into an unmanaged environment, what/how do we project?
Examples: Type
-> CCW (built-in COM) or Type
-> Type
(Where the second Type
is the ABI projection, for example WinRT).
Both scenarios above can be relatively simple to solve in a JIT-supported .NET environment. However, for AOT scenarios, where size is a consideration, the above mapping must be trimmable and the simple type mapping solutions using tools like Type.GetType()
are not viable.
This API is designed to both support the Type Mapping needed for interop as well as be trimmable for AOT scenarios.
See #110691 for complete details.
Comment describing the workflow this supports - #110691 (comment)
API Proposal
namespace System.Runtime.InteropServices;
/// <summary>
/// Type mapping between a string and a type.
/// </summary>
/// <typeparam name="TTypeUniverse">Type map universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class TypeMapAttribute<TTypeUniverse> : 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="TTypeUniverse">Type map universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class TypeMapAssemblyTargetAttribute<TTypeUniverse> : 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="TTypeUniverse">Type map universe</typeparam>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class TypeMapAssociationAttribute<TTypeUniverse> : 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="TTypeUniverse">Type map universe</typeparam>
/// <returns>Requested type map</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<TTypeUniverse>();
/// <summary>
/// Returns the associated type type map generated for the current application.
/// </summary>
/// <typeparam name="TTypeUniverse">Type map universe</typeparam>
/// <returns>Requested type map</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<TTypeUniverse>();
}
API Usage
Provided by .NET for Android runtime
// Type used to express specific type map universe.
public sealed class JavaTypeMapUniverse { }
// Java capability to enable type instance creation.
public interface IJavaCreateInstance
{
object Create(ref JniObjectReference reference, JniObjectReferenceOptions options);
}
// Java capability implementation for object creation.
public class JavaObjectTypeMapAttribute<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T> : Attribute, IJavaCreateInstance
where T : JavaObject
{
public object Create(ref JniObjectReference reference, JniObjectReferenceOptions options)
{
// Create instance using T.
}
}
// Java capability implementation for string creation.
public class JavaStringTypeAttribute : Attribute, IJavaCreateInstance
{
public object Create(ref JniObjectReference reference, JniObjectReferenceOptions options)
{
IntPtr ptr = /* Extract the underlying string pointer from reference. */
string str = Marshal.PtrToStringUni(ptr);
return str;
}
}
// -- Alternative attribute design
// This would expose the direct as a Type instead of as a generic parameter. It would reduce
// type loading costs and could be the single attribute for all "capabilities".
public class JavaObjectTypeMapAttribute : Attribute
{
public JavaObjectTypeMapAttribute([DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)
{ }
public [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type TargetType { get; }
}
.NET for Android projection library
// Dependent association between typeof(string) => typeof(StringProxy).
[assembly: TypeMapAssociation<JavaTypeMapUniverse>(typeof(string), typeof(StringProxy))]
// Map string name of Java type to typeof(StringProxy), using typeof(string) to indicate
// if the entry should be included or not.
[assembly: TypeMap<JavaTypeMapUniverse>("java/lang/String", typeof(StringProxy), typeof(string))]
[JavaStringType]
public class StringProxy
{ }
User application
// Application reference for assembly to be used in type map generation.
[assembly: TypeMapAssemblyTarget<JavaTypeMapUniverse>("Application.SideCar.dll")]
// User defined type
[JavaObjectTypeMap<MyJavaClass>]
public class MyJavaClass : JavaObject
{ }
var inst = new ClassFromNuGet();
Generated Application.SideCar.dll
A sidecar assembly could always be generated to handle the following:
- Specify assembly type map targets (for example, .NET for Android runtime)
- Specify type mappings for types defined in the application.
- Generate proxies and mappings for types in Nugets to support older TFMs.
// Application reference for assembly to be used in type map generation.
[assembly: TypeMapAssemblyTarget<JavaTypeMapUniverse>("Android.Runtime.dll")]
[assembly: TypeMap<JavaTypeMapUniverse>("app/MyJavaClass", typeof(MyJavaClass), typeof(MyJavaClass))]
[assembly: TypeMap<JavaTypeMapUniverse>("lib/classFromNuget", typeof(ClassFromNuGetProxy), typeof(ClassFromNuGet))]
[JavaObjectTypeMap<ClassFromNuGet>]
internal class ClassFromNuGetProxy
{ }
Interop runtime (for example, .NET for Android) usage example.
// Stored by the interop runtime in some global static
static IReadOnlyDictionary<string, Type> s_JavaTypeMap = TypeMapping.GetOrCreateProxyTypeMapping<JavaTypeUniverse>();
...
string javaTypeName = /* Get Java type name from jniHandle */
s_JavaTypeMap.TryGetValue(javaTypeName, out Type? projection);
Debug.Assert(projection != null); // Assuming map contains type.
var creator = (IJavaCreateInstance)projection.GetCustomAttribute(typeof(JavaObjectTypeMapAttribute<>));
var objectRef = new JniObjectReference(jniHandle, JniObjectReferenceType.Local);
JavaObject mcw = (JavaObject)creator.Create(ref objectRef, JniObjectReferenceOptions.None);
Alternative Designs
API design alternatives:
- The APIs on
TypeMapping
could instead be getter methods instead of exposing theIReadOnlyDictionary<,>
. - The APIs on
TypeMapping
could avoid theTry
semantics and always return theIReadOnlyDictionary<,>
since there is expected to be a dynamic solution that will create theIReadOnlyDictionary<,>
at run-time. - Generics could be removed from several APIs to avoid the additional loading of types for each universe combination.
API alternative
Continue with the bespoke solutions that currently exist in CsWinRT, Android (Java), Swift, and future interop platform targets.
Risks
This has a minimal risk to the current ecosystem as it is new.
The larger risk is creating something that doesn't address all issues for current or future interop platform targets. At present, there is work being done CsWinRT to confirm the mechanics. The Android (Java) target is actively being explored with no obvious concerns. The Trimmer team has been consulted extensively about this API and signed off from the trimmable perspective.
Related to missing a functional need, there is a potential to be less performant than the current bespoke solutions being employed. This performance gap could impact adoption.
Finally, since the trimmable semantics are very strict in this model, it is possible incorrectness in existing TypeMap implementations may be uncovered resulting in new issues for adopting interop frameworks.
Metadata
Metadata
Assignees
Type
Projects
Status