Skip to content

Conversation

@simonrozsival
Copy link
Member

@simonrozsival simonrozsival commented Feb 3, 2026

This specification defines the architecture for enabling Java-to-.NET interoperability in .NET Android applications using the .NET Type Mapping API. The design is fully compatible with Native AOT and trimming.

  • AOT-Safe: All type instantiation and method resolution works with Native AOT
  • Trimming-Safe: Proper annotations ensure required types survive aggressive trimming
  • Developer Experience: No changes required to existing .NET Android application code

Expands on dotnet/runtime#120121

Proof of concept

See https://github.com/dotnet/android/compare/dev/simonrozsival/trimmable-typemap

Core idea

Each Java peer type is registered using assembly-level attributes:

// TypeMap<TUniverse>(string jniClassName, Type proxyType, Type trimTarget)
// - jniClassName: Java class name used as lookup key
// - proxyType: The proxy type RETURNED by TypeMap lookups
// - trimTarget: Ensures trimmer preserves mapping when target is used

[assembly: TypeMap<Java.Lang.Object>("com/example/MainActivity", typeof(MainActivity_Proxy))] // no trim target -> unconditionally preserved
[assembly: TypeMap<Java.Lang.Object>("com/example/MainActivity", typeof(MainActivity_Proxy), typeof(MainActivity))] // trim target -> type map record will be trimmed if trim target is trimmed

The *_Proxy types are generated attribute classes which is applied to itself:

// Proxy applies ITSELF as an attribute to ITSELF
[MainActivity_Proxy]  // Self-application
public sealed class MainActivity_Proxy : JavaPeerProxy, IAndroidCallableWrapper
{
    public override IJavaPeerable CreateInstance(IntPtr handle, JniHandleOwnership transfer)
        => new MainActivity(handle, transfer);
    
    public override JavaPeerContainerFactory GetContainerFactory()
        => JavaPeerContainerFactory.Create<MainActivity>();
    
    // IAndroidCallableWrapper - only on ACW types
    public IntPtr GetFunctionPointer(int methodIndex) => methodIndex switch {
        0 => (IntPtr)(delegate* unmanaged<...>)&n_OnCreate,
        _ => IntPtr.Zero
    };
    
    // marshal methods...
    [UnmanagedCallersOnly]
    public static void n_OnCreate(...) { ... }
}

// At runtime:
Type proxyType = typeMap["com/example/MainActivity"];  // Returns typeof(MainActivity_Proxy)
JavaPeerProxy proxy = proxyType.GetCustomAttribute<JavaPeerProxy>();  // Returns MainActivity_Proxy instance
IJavaPeerable instance = proxy.CreateInstance(handle, transfer);  // Returns MainActivity instance

Why this works:

  1. TypeMap returns the proxy type, not the target type
  2. The .NET runtime's GetCustomAttribute<T>() instantiates attributes in an trimming and AOT-safe manner
  3. The trimTarget parameter ensures the mapping is preserved when the target type survives trimming

/cc @jtschuster


Related


- Debug and Release builds using CoreCLR or NativeAOT runtime
- All Java peer types: user classes, SDK bindings, interfaces with invokers
- `[Register]` and `[Export]` attribute methods
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we are already considering [Export] not trimmer safe at all -- it currently emits trimmer warnings.

It isn't a required feature for Android, as you can just make an interface instead, and the entire API will be strongly typed. [Export] is basically C# -> strings -> generate a Java class/method with your string.

An example of removing [Export]:

So, we could consider improving [Export] future, out of scope.

@rolfbjarne
Copy link
Member

I think this looks great, and I don't see any major problems for macios either!

Comment on lines +1043 to +1044
**Key Insight:** In the legacy system, most types are only preserved if they're referenced by user code. The legacy `MarkJavaObjects` only unconditionally marks:
1. Types with `[Activity]`, `[Service]`, `[BroadcastReceiver]`, `[ContentProvider]`, `[Application]`, `[Instrumentation]` attributes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you could technically, not use these attributes and put them in your AndroidManifest.xml file manually.

That means that AndroidManifest.xml can define additional "roots" other than the application assembly. I don't think people would commonly do this, but maybe we should mention it's not trimmer safe -- they'd need to preserve the type another way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already scan layouts and root the types that appear in those XML files. We can easily scan AndroidManifest.xml as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants