Skip to content

[TrimableTypeMap] Reuse base UCO wrappers for inherited overrides#11466

Open
simonrozsival wants to merge 7 commits into
mainfrom
dev/simonrozsival/uco-overridden-virtuals-register-natives
Open

[TrimableTypeMap] Reuse base UCO wrappers for inherited overrides#11466
simonrozsival wants to merge 7 commits into
mainfrom
dev/simonrozsival/uco-overridden-virtuals-register-natives

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented May 23, 2026

Summary

Avoid generating duplicate [UnmanagedCallersOnly] wrappers for inherited virtual override marshal methods in the trimmable typemap assembly.

For compatible inherited overrides, derived RegisterNatives entries now point at the base proxy UCO wrapper target. The base callback still does Object.GetObject<BaseType> (...) and calls the managed virtual method, so managed virtual dispatch reaches the derived override without an extra generated UCO wrapper per derived type.

The reuse envelope is intentionally conservative:

  • exact callback type/method/JNI signature match
  • no constructors
  • no [Export] direct-dispatch methods
  • no generic proxy or generic callback targets
  • fallback to the existing local wrapper when no compatible base wrapper target is emitted in the current typemap assembly

The emitter now also orders generated proxy types by wrapper-target dependency before emitting method bodies. This keeps derived RegisterNatives emission order-independent: a derived proxy can safely reference a base proxy UCO wrapper even when the derived proxy appears earlier in the input model.

Generated shape

The Java surface still has one native method per generated Java type. The change is only the function pointer each generated proxy passes to RegisterNatives:

abstract class UcoOverrideBase extends android.view.View {
    private native void n_doWork0();
}

final class UcoOverrideDerived0 extends UcoOverrideBase {
    private native void n_doWork0();
}

Before this change, the derived proxy registered UcoOverrideDerived0.n_doWork0() against its own duplicate UCO wrapper:

// Decompiled shape from _TypeMap.Proxies.MyApp_UcoOverrideDerived0_Proxy
public unsafe void RegisterNatives (JniType nativeClass)
{
    JniNativeMethod* methods = stackalloc JniNativeMethod [2];
    methods [0] = new JniNativeMethod (
        "n_doWork0",
        "()V",
        (IntPtr)(delegate*<IntPtr, IntPtr, void>)(&n_doWork0_uco_0));
    methods [1] = new JniNativeMethod (
        "<init>",
        "()V",
        (IntPtr)(delegate*<IntPtr, IntPtr, void>)(&nctor_0_uco));

    Types.RegisterNatives (nativeClass.PeerReference, new ReadOnlySpan<JniNativeMethod> (methods, 2));
}

After this change, the derived proxy keeps the same Java native registration, but the method callback points at the base proxy's UCO wrapper:

// Decompiled shape from _TypeMap.Proxies.MyApp_UcoOverrideDerived0_Proxy
public unsafe void RegisterNatives (JniType nativeClass)
{
    JniNativeMethod* methods = stackalloc JniNativeMethod [2];
    methods [0] = new JniNativeMethod (
        "n_doWork0",
        "()V",
        (IntPtr)(delegate*<IntPtr, IntPtr, void>)(&MyApp_UcoOverrideBase_Proxy.n_doWork0_uco_0));
    methods [1] = new JniNativeMethod (
        "<init>",
        "()V",
        (IntPtr)(delegate*<IntPtr, IntPtr, void>)(&nctor_0_uco));

    Types.RegisterNatives (nativeClass.PeerReference, new ReadOnlySpan<JniNativeMethod> (methods, 2));
}

The shared base wrapper still calls the base registered callback; that callback performs Object.GetObject<BaseType> (...), then the managed virtual call dispatches to the derived override:

public static void n_doWork0_uco_0 (IntPtr jnienv, IntPtr self)
{
    AndroidRuntimeInternal.WaitForBridgeProcessing ();
    try {
        UcoOverrideBase.n_DoWork0 (jnienv, self);
    } catch (Exception ex) {
        AndroidEnvironmentInternal.UnhandledException (ex);
    }
}

Measurements

In a synthetic app-shaped typemap with one registered abstract base and 20 registered derived types overriding 12 virtual methods:

  • Method UCO wrappers: 252 -> 12
  • Typemap DLL: 51,712 B -> 30,720 B (-20,992 B, -40.6%)
  • Stripped osx-arm64 NativeAOT proxy executable: 1,136,000 B -> 1,102,752 B (-33,248 B, -2.93%)

That works out to roughly 87 B saved in the CoreCLR typemap DLL and 139 B in the stripped NativeAOT proxy executable per removed derived-method UCO wrapper. These are directional estimates; actual app savings depend on metadata/native layout and alignment.

Tests

  • dotnet test tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj --no-restore (556 passed, run with .NET SDK 10.0.101 from a session-local global.json)

Added model/emitted-IL coverage for inherited override reuse, including the derived-before-base input-order case and intermediate callback-owner case, plus a focused CoreCLR/trimmable MSBuildDeviceIntegration test for Java-to-native dispatch through the shared base UCO path. I could not run the device test locally because this worktree's repo-prep/device-test build is blocked by local tooling/generated-artifact issues (make prepare fails under the local .NET 11 preview SDK; the device-test build then hits missing javac, XABuildConfig.cs, Xamarin.Android.Tools.BootstrapTasks.dll, and manifestmerger Gradle failures).

Avoid emitting duplicate UnmanagedCallersOnly wrappers for inherited virtual override marshal methods when a compatible base wrapper is already emitted in the generated typemap assembly. Register derived Java native methods against the base wrapper target and rely on managed virtual dispatch from the base callback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 23, 2026 06:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR reduces codegen bloat in the trimmable typemap assembly by reusing a base type’s existing [UnmanagedCallersOnly] (UCO) wrapper for compatible inherited virtual overrides, so derived types’ RegisterNatives can point at the base wrapper instead of generating duplicate wrappers.

Changes:

  • Update typemap model + emitter to track RegisterNatives wrapper targets (type + method), enabling cross-proxy wrapper reuse.
  • Implement conservative wrapper-reuse logic in the model builder and remove now-redundant derived UCO wrappers.
  • Add unit tests (model + assembly IL validation) and a new device integration test covering Java→native→managed dispatch via the shared base UCO path.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs Adds device test validating inherited override dispatch using the shared base UCO wrapper under CoreCLR + trimmable typemap.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs Adds model-level tests for reuse, ordering, and fallback behavior.
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs Adds IL-level test ensuring derived RegisterNatives references the base UCO wrapper method.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs Switches wrapper handle tracking to use wrapper targets and updates RegisterNatives emission to resolve by target.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Builds native registrations in a post-pass with wrapper reuse and removes reused derived wrappers.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs Introduces UcoWrapperTargetData and stores wrapper targets in NativeRegistrationData.

@simonrozsival simonrozsival changed the title Reuse base UCO wrappers for inherited overrides [TrimableTypeMap] Reuse base UCO wrappers for inherited overrides May 23, 2026
Ensure proxy types that own reused UCO wrapper targets are emitted before proxy types whose RegisterNatives methods reference those wrappers. This keeps inherited override reuse independent of model proxy ordering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

/review

simonrozsival and others added 4 commits May 23, 2026 09:55
Cover an A : B : C chain where A overrides a method declared by C and A RegisterNatives reuses C's UCO wrapper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover an inheritance chain where both the root and intermediate types have same-signature callbacks, and a leaf override must reuse the intermediate UCO rather than the root UCO.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover hidden virtual slots that reuse the same JNI signature, including the missing intermediate proxy fallback path. This ensures generated UCO wrappers keep calling the recorded callback owner instead of accidentally reusing a root-base wrapper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clarify the callback member reference handle helper name and share inline method token scanning between call and ldftn assertions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

@copilot there is a bug somewhere, find it and fix it:

TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco(CoreCLR)

Xamarin.ProjectTools.FailedBuildException : Build failure: UnnamedProject.csprojBuild log recorded at /Users/runner/work/1/a/TestRelease/05-23_09.46.27/temp/TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUcoCoreCLR/build.log

   at Xamarin.ProjectTools.Builder.BuildInternal(String projectOrSolution, String target, String[] parameters, Dictionary`2 environmentVariables, Boolean restore, String binlogName) in /Users/builder/azdo/_work/2/s/android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/Builder.cs:line 417
   at Xamarin.ProjectTools.ProjectBuilder.Build(XamarinProject project, Boolean doNotCleanupOnUpdate, String[] parameters, Boolean saveProject, Dictionary`2 environmentVariables) in /Users/builder/azdo/_work/2/s/android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs:line 137
   at Xamarin.ProjectTools.ProjectBuilder.RunTarget(XamarinProject project, String target, Boolean doNotCleanupOnUpdate, String[] parameters, Dictionary`2 environmentVariables, Boolean saveProject) in /Users/builder/azdo/_work/2/s/android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs:line 190
   at Xamarin.ProjectTools.ProjectBuilder.Install(XamarinProject project, Boolean doNotCleanupOnUpdate, String[] parameters, Boolean saveProject) in /Users/builder/azdo/_work/2/s/android/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/ProjectBuilder.cs:line 149
   at Xamarin.Android.Build.Tests.InstallAndRunTests.TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco(AndroidRuntime runtime) in /Users/builder/azdo/_work/2/s/android/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs:line 272
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)

@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels May 23, 2026
Agent-Logs-Url: https://github.com/dotnet/android/sessions/80eb25cb-843f-4ce1-975d-406fac750920

Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 23, 2026

@copilot there is a bug somewhere, find it and fix it:

TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco(CoreCLR)

...

Fixed in 70b9f8f. The device test was generating Java methods for app-defined doWork/doHiddenWork on java.lang.Object subclasses, which produced invalid @Override annotations during javac. The test now uses hashCode()/GetHashCode() so the Java override is valid while still exercising the shared inherited UCO wrapper path.

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

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants