Skip to content

[API Proposal]: Type Mapping API #113362

Closed

Description

@AaronRobinsonMSFT

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 the IReadOnlyDictionary<,>.
  • The APIs on TypeMapping could avoid the Try semantics and always return the IReadOnlyDictionary<,> since there is expected to be a dynamic solution that will create the IReadOnlyDictionary<,> 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.

Activity

  1. added this to the 10.0.0 milestone on Mar 11, 2025
  2. dotnet-policy-service commented on Mar 11, 2025

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @dotnet/interop-contrib
    See info in area-owners.md if you want to be subscribed.

  3. AaronRobinsonMSFT commented on Mar 11, 2025

    @AaronRobinsonMSFT
    MemberAuthor

    @agocke @sbomer @rolfbjarne @jonpryor @jkoritzinsky @MichalStrehovsky @Sergio0694 @jkotas @simonrozsival @ivanpovazan

    I'd like to get this API approved ASAP. Please prioritize questions/comments/concerns.

  4. Sergio0694 commented on Mar 11, 2025

    @Sergio0694
    Contributor

    From the CsWinRT side, we've been busy prototyping CsWinRT 3.0 on top of these APIs (see fhl/cswinrt-3.0), and at least from a usability perspective, we're actually quite happy with how well the design integrates with things on our end. Obligatory: we couldn't actually test this (for obvious reasons), but the API shape seems to cover all scenarios we've looked at so far 😄

    A couple notes while at it:

    • I have a slight preference for GetExternalTypeMapping returning the type map directly, over TryGetExternalTypeMapping, if we decide to just always let the trimmer build a map or throw an exception if it's missing. Mostly because it makes it a little bit more convenient to use it in static field initializers (since my understanding is, you should cache the type map returned from those methods?). Otherwise yeah we can also live with the TryGet___ pattern if there's a strong preference.
    • I was talking with @MichalStrehovsky and he mentioned the linker might have to be a bit conservative with respect to trimming entries in the external type map in some cases, causing it to keep more entries for some types even if there's no actual type check for them anywhere. I suppose this is more related to docs, but it'd be nice to have a detailed explanation somewhere of what patterns one should try to prefer or avoid to make the trimmer be more effective here, what could cause it to root unwanted things, etc.
    • Similar to the previous point, I'd like some more clarify on how generics come into play here. In CsWinRT, we have a bunch of helpers where it'd be convenient for us to have a generic method which also does some casting (eg. think return (T)foo). I understand that the linker wouldn't "see" those casts with respect to the external type table, which is great, exactly what we want (we should document this too). But are there other considerations with respect to casts in generics? Would they cause the trimmer to keep more stuff in either type map, and if so, in what scenarios exactly?

    Really looking forward to this API, thank you again @AaronRobinsonMSFT for driving this!

  5. MichalStrehovsky commented on Mar 11, 2025

    @MichalStrehovsky
    Member

    I have a slight preference for GetExternalTypeMapping returning the type map directly, over TryGetExternalTypeMapping, if we decide to just always let the trimmer build a map or throw an exception if it's missing. Mostly because it makes it a little bit more convenient to use it in static field initializers (since my understanding is, you should cache the type map returned from those methods?). Otherwise yeah we can also live with the TryGet___ pattern if there's a strong preference.

    I was thinking about that one too. Unless there's a special need for the API to return a dictionary, it might be better to do:

        public static bool TryGetExternalTypeMapping<TTypeUniverse>(string id, [NotNullWhen(returnValue: true)] out Type type)
            where TTypeUniverse : ITypeMapUniverse;
    
        public static bool TryGetTypeProxyMapping<TTypeUniverse>(Type key, [NotNullWhen(returnValue: true)] out Type value)
            where TTypeUniverse : ITypeMapUniverse;

    Returning an IReadOnlyDictionary makes it possible for someone to write code that iterates it or counts things in it and unless that's needed for something, it's just a way to write code that may not behave the same before/after trimming.

    I was talking with @MichalStrehovsky and he mentioned the linker might have to be a bit conservative with respect to trimming entries in the external type map in some cases, causing it to keep more entries for some types even if there's no actual type check for them anywhere. I suppose this is more related to docs, but it'd be nice to have a detailed explanation somewhere of what patterns one should try to prefer or avoid to make the trimmer be more effective here, what could cause it to root unwanted things, etc.

    I don't know if this is something we could contractually guarantee. We'd consider things casted to something whenever it's possible in some way to get there. For example #110691 (comment), or IsAssignableFrom APIs, etc.

    Similar to the previous point, I'd like some more clarify on how generics come into play here. In CsWinRT, we have a bunch of helpers where it'd be convenient for us to have a generic method which also does some casting (eg. think return (T)foo). I understand that the linker wouldn't "see" those casts with respect to the external type table, which is great, exactly what we want (we should document this too). But are there other considerations with respect to casts in generics? Would they cause the trimmer to keep more stuff in either type map, and if so, in what scenarios exactly?

    It's my understanding that this is for casts and cast-like behaviors in user code, not just within the interop layer.

    Casts in generic code are still casts. If we were to treat it as anything else, it would make things difficult to reason about. Trimming will need to be more conservative to account for generics. It already does that to keep casting in general working. I think the general rule we'll need to end up with is "if you can obtain a System.Type of the type at runtime, it should also be considered used in a cast", modulo maybe things like System.Type used in a ==/!= comparison. Whenever we optimize away a "cast", it's a potential after-trimming behavioral difference, so we'd want as few of them as possible, ideally none.

    The proposal LGTM otherwise!

  6. rolfbjarne commented on Mar 11, 2025

    @rolfbjarne
    Member

    I think I see a potential problem, where the assembly attributes can't see nested private types for any typeof expressions:

    // this won't compile
    [assembly: TypeMap<ObjectiveCTypeUniverse> ("MyInnerClass", typeof (MyClass.MyInnerClass))]
    
    class MyClass {
        class MyInnerClass : NSObject {}
    }

    and this won't work either:

    class MyClass {
    	// this won't compile
    	[assembly: TypeMap<ObjectiveCTypeUniverse> ("MyInnerClass", typeof (MyClass.MyInnerClass))]
        class MyInnerClass : NSObject {}
    }

    I don't think this would be a problem in IL though, which is probably what we're going to do anyways (emit IL instead of C# code).

  7. Sergio0694 commented on Mar 11, 2025

    @Sergio0694
    Contributor

    it might be better to do [...]

    If we want to do that (I'm not opposed), at that point can we just use a span too for the external type mapping? 😄

    public static bool TryGetExternalTypeMapping<TTypeUniverse>(ReadOnlySpan<char> id, [NotNullWhen(returnValue: true)] out Type type)
        where TTypeUniverse : ITypeMapUniverse;

    I understand the lookup might still be expensive, but this would still feel like a small free perf win to me. It would've been different if we had wanted to return a dictionary, in which case, sure. But if we want to expose lookup methods directly, why not?

    "I think the general rule we'll need to end up with is "if you can obtain a System.Type of the type at runtime, it should also be considered used in a cast"

    Mmmhhh... I'm kinda starting to worry that putting everything together this might actually end up basically just rooting pretty much almost everything. I suppose the only way to actually find out is to actually try things out for real with a CsWinRT prototype and see what happens, and possibly go from there and see what could be improved. Hopefully it's not actually that bad 😅

    "I think I see a potential problem, where the assembly attributes can't see nested private types for any typeof expressions"

    You'll likely need to have an analyzer in your interop layer emitting an error for such cases, and telling users that they need their types needing marshalling to be at least accessible to the assembly. Eg. I do the same in ComputeSharp for a similar reason.

  8. agocke commented on Mar 11, 2025

    @agocke
    Member

    I shared some feedback offline, I'll copy it here as well.

    I have the same concern as @MichalStrehovsky above -- now that we've moved towards generating a universal implementation using reflection, I don't think producing an IReadOnlyDictionary is helpful, and it may be harmful because it exposes more of the implementation than we want.

    I also think the combination of generics and reflection is a little awkward. We should consider passing ITypeMapUniverse as either a System.Type or even just a string. A typeof in an attribute is just a string anyway.

  9. jkotas commented on Mar 11, 2025

    @jkotas
    Member

    now that we've moved towards generating a universal implementation using reflection

    My understanding that generation of universal implementation using reflection is not part of the current proposal.

    • If the type map is generated at build time by the AOT compiler, the TryGet... APIs return true and the type map IDictionary.
    • If the type map is not generated at build time by the AOT compiler, the TryGet... APIs return false and the specific interop runtime has to use some fallback solution. This fallback is not provided by the runtime.

    @AaronRobinsonMSFT Is this correct?

  10. jkotas commented on Mar 11, 2025

    @jkotas
    Member

    Returning an IReadOnlyDictionary makes it possible for someone to write code that iterates it or counts things in it

    We do not have to implement all methods on IReadOnlyDictionary. It is fine to implement IReadOnlyDictionary methods that we do not like as throw NotSupportedException.

  11. AaronRobinsonMSFT commented on Mar 11, 2025

    @AaronRobinsonMSFT
    MemberAuthor

    I think I see a potential problem, where the assembly attributes can't see nested private types for any typeof expressions:

    @rolfbjarne Yes, that would be an issue. Is this a common pattern? I think it would be an acceptible limitation and as you mention, emitting IL directly is at least a workaround.

    Returning an IReadOnlyDictionary makes it possible for someone to write code that iterates it or counts things in it and unless that's needed for something, it's just a way to write code that may not behave the same before/after trimming.

    @agocke @MichalStrehovsky @Sergio0694 This has evolved for me and I now prefer returning the IReadOnlyDictionary<,>. I think it is the common shape we want and even though there are APIs that could be ill-advised now, we can throw as suggested in #113362 (comment).

    I also think the combination of generics and reflection is a little awkward. We should consider passing ITypeMapUniverse as either a System.Type or even just a string. A typeof in an attribute is just a string anyway.

    I don't see that combination here. The ITypeMapUniverse is a generic parameter that tells the system which IReadOnlyDictionary<,> to return. We can of course create an arbitrary key mechanism via a string but that seems a bit fragile. I can get behind moving it from a generic parameter to a ITypeMapUniverse instance argument if that is preferred though. My desire here is for arbitary interop frameworks to own how they describe their universe. The only way for someone to look-up the type map for an Android scenario, would be through a key the Android team defines.

  12. 57 remaining items

  13. Sergio0694 commented on Mar 30, 2025

    @Sergio0694
    Contributor

    "I had to wait for static interface members to become available in order to implement my mapping approach, using an intermediary metadata class to invoke constructors and perform other necessary operations."

    @josephmoresena out of curiosity, would you be able to link to the logic you use (or some examples) to handle marshalling objects to the right types when no static type information is available? That is, say you have a native API that returns just object, where that object (at runtime) is actually some specific native object of some type (say, Foo.Bar). When marshalling to managed, how do you lookup the type specific marshalling logic for Foo.Bar so you can construct an instance of that type? And also how do you do this in a trimmable way? 🤔

  14. josephmoresena commented on Mar 31, 2025

    @josephmoresena
    Contributor

    When marshalling to managed, how do you lookup the type specific marshalling logic for Foo.Bar so you can construct an instance of that type? And also how do you do this in a trimmable way? 🤔

    JNetInterface doesn’t rely on traditional marshalling. Instead, my implementation retrieves the class of the Java object at runtime and determines the most compatible managed type available.

    I’ve tried to explain the core ideas in the following documentation:

    To resolve metadata, I use a static dictionary populated at runtime. The key is the type’s hash, which, in this case, is a System.String instance used as buffer for the UTF-8 characters of the type’s JNI name.

    Each type’s interface defines three static methods that are required to be implemented. These are used by the generic metadata logic to create instances dynamically:

    This approach is fully trimmable compatible, as it only depends on the code paths actually used. Even when reflection is not disabled (i.e., the feature switch to avoid it is not enabled), metadata for jagged arrays like T[]...[] can still be generated at runtime without issues. I wasn’t able to avoid reflection entirely due to the (N+1) problem when dealing with these array types.

    A consumer-facing example of the mapping can be found here:

    That said, it's worth mentioning that the built-in types in JNetInterface use a slightly different initialization strategy:

    This alternative method, based on precomputed constants, is likely to yield better performance during initialization.

  15. Sergio0694 commented on Apr 11, 2025

    @Sergio0694
    Contributor

    Closing the loop on this, I skipped the links you shared (thank you!). I think this is the main point:

    "To register a data type, use the static method JVirtualMachine.Register<T>()."

    Now I understand where the disconnect is (probably). This interop mapping system seems to be:

    • Explicit (you have to manually register the projected/mapped types you will be using)
    • Untrimmable (if you register a type but don't actually use it, it's still rooted)

    This (especially the first point) are fundamentally incompatible and different with what's proposed here. The point of the type map APIs is to allow projection systems (eg. WinRT, Java, etc.) to declare managed versions of native types, in such a way that:

    1. No manual registration is required.
    2. You automatically get the right type from a given opaque native object being marshalled back.
    3. All projected types are fully trimmable.
    4. You can correctly and once again automatically marshal user-defined managed types. This includes normal user defined types (possibly implementing projected interfaces), composed types, and generic instantiations.

    The external type map gives you (1), (2) for projected types, and (3).
    The proxy type map gives you (2) for user defined types, and (3).

    Not saying either approach is necessarily better, just that they seem to have a very different design (and ergonomics). I don't think you can achieve all 4 points mentioned above with a fully statically typed system, because you fundamentally can't know all static type information at all time (ie. you can have a native API that takes or returns object, and you might want to downcast it).

  16. josephmoresena commented on Apr 15, 2025

    @josephmoresena
    Contributor

    The external type map gives you (1), (2) for projected types, and (3). The proxy type map gives you (2) for user defined types, and (3).

    Yes, I believe that in my case, item 1 would be the most important for me. JNI calls are not fixed, as they depend on the specific invocation made to or from Java.
    Marshalling could be a plus—it might improve the experience when declaring native Java methods, but I think using an adapter is acceptable in that context.

    Item 3 in my approach holds true as long as the different execution flows are not used. While it's true that once a type is registered it remains available, in practice, only the functionality that was actually used is preserved.
    However, I wanted to find a way to optimize the registration process and, if possible, avoid doing it manually. Although duplicate registration is not allowed—and does not throw any error when attempted—it is mainly intended for use by a module initializer, which only makes sense (or makes much more sense) in executable assemblies. This leaves library assemblies somewhat unsupported—at least formally—although it can be addressed through custom strategies defined by each user.

    Additionally, there’s the matter of the TypeMetadataBuilder. While I consider it a great fluent alternative at design time, at runtime it's problematic—it should ideally be a compile-time construct. That’s precisely why I’m interested in this strategy, even though marshalling is not one of my goals.

  17. agocke commented on Apr 15, 2025

    @agocke
    Member

    I think @josephmoresena's approach has a lot of benefits. The main one is: although the static methods are structured as separate to execution, they don't have to be. For example, if JVirtualMachine.Register<T>() were called next to the place that the target type were used, then that call would be subject to trimming in the same way as the code using it.

    What would be ideal is some system where the registration returns some sort of provider, which is actually needed to produce an object of that type. That would mean that casts would only succeed when they go through registration, and there would be a statically typed accessor that enforces that everything goes through registration.

    However, this doesn't solve the same problem that the type map solves, which primarily is: back compat. The type map allows things like is and as checks to succeed without special handling. However, it's also unsafe. Things like object.GetType() can return different results based on trimming. The system @josephmoresena is using is safer and more predictable.

  18. Sergio0694 commented on Apr 15, 2025

    @Sergio0694
    Contributor

    Can you elaborate why the type map is inherently more unsafe? I'm not sure I get the difference between:

    • All casts (as, is, (T), etc.) are recognized by linker, which roots the marshalling code for it
    • You explicitly casts with some eg. Cast<T>() method, which roots and registers the T from there

    Like, in both cases all those casts would always work. I understand that it's not "safe" in the sense that you might get slightly different behavior when trimming, but I guess I'm wondering, isn't this at least "safer" than normal trim-unsafe code, in the sense that once you do enable trimming, the behavior is fully deterministic, since the linker will always root the same external types in the map based on the casts you actually do, and not just on "whatever code happens to accidentally be rooted" in the app? 🤔

  19. agocke commented on Apr 15, 2025

    @agocke
    Member

    Like, in both cases all those casts would always work

    You’re assuming that the system preserves object identity when using the strong cast and that the is check works too. I wasn’t proposing that.

  20. Sergio0694 commented on Apr 15, 2025

    @Sergio0694
    Contributor

    Ah, I see, I'm following now. Yeah for us (CsWinRT) that's an important aspect of this implementation, as code will just rely on this all over the place. I can see how an approach where you just have to do Cast<T>() and that might return a new RCW instance every time would be much simpler to statically implement (though eg. simply way too impractical in our case).

  21. MichalStrehovsky commented on Apr 15, 2025

    @MichalStrehovsky
    Member

    Like, in both cases all those casts would always work

    We were explicitly discussing that casts would not always work in places like generic code (casting to T when T is a generic parameter). Cast<T> would have the same problem when it comes to generics, but the main difference is that the underlying interop stack could warn whenever it saw Cast<T> being used with a generic T. It cannot warn for (T)/is T/as T because that would simply not be practical (most of the time this cast is not an "interop operation").

    So an explicit Cast<T> that returns a new RCW every time the T changes allows implementing this in a trimming safe manner. The heuristic that makes is sometimes work will always have holes (which is why the proposal marks these APIs as unsafe - the underlying interop stack will need to suppress the warning, but my position is that suppressing the warning will always be a bug since the underlying interop stack cannot guarantee these semantics, but at least we can wash our hands off it on the trimming side).

  22. Sergio0694 commented on Apr 16, 2025

    @Sergio0694
    Contributor

    "We were explicitly discussing that casts would not always work in places like generic code"

    To clarify, I meant with the revised trimming policy you mentioned, where the external type map would also consider all generic type arguments as potentially used in a cast, and preserve them as entries in the table. So those generic casts would also still work.

  23. Sergio0694 commented on Apr 29, 2025

    @Sergio0694
    Contributor

    Small question: how does one suppress warnings when using the assembly attributes here? 🤔

    Image

    Like, I can just use [UnconditionalSuppressMessage] when using the TypeMapping APIs, that part is fine. But I'm not sure how to handle the warnings from the actual attributes. You can use the same attribute on the assembly, but that will suppress all trim warnings anywhere else too, which can hide actual errors and doesn't seem great. You can't just use #pragma warning disable here in source, because that won't actually stop ILLink/ILC from emitting those warnings when you actually publish.

    ...Could we perhaps drop [RequiresUnreferencedCode] on the attributes? All TypeMapping APIs are annotated anyway, so you'd still get warnings when using them. Would that not be sufficient? Applying the attribute itself doesn't seem trim-unsafe.

  24. MichalStrehovsky commented on Apr 29, 2025

    @MichalStrehovsky
    Member

    Small question: how does one suppress warnings when using the assembly attributes here? 🤔

    I don't think you can. This will require special casing in trimming. Discussion here: #110691 (comment)

  25. Sergio0694 commented on Apr 29, 2025

    @Sergio0694
    Contributor

    Ah, I see, I had missed that message. I'll just ignore them for now then, thank you! 🙂

  26. locked and limited conversation to collaborators on Jul 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    • Status
      No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions