From f583111fef4d634e0be0420128a888e359012be5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 08:26:33 +0200 Subject: [PATCH 1/8] Reuse base UCO wrappers for inherited overrides 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> --- .../Generator/Model/TypeMapAssemblyData.cs | 27 +++- .../Generator/ModelBuilder.cs | 110 +++++++++++--- .../Generator/TypeMapAssemblyEmitter.cs | 23 ++- .../Tests/InstallAndRunTests.cs | 106 ++++++++++++++ .../TypeMapAssemblyGeneratorTests.cs | 90 ++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 134 ++++++++++++++++++ 6 files changed, 464 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 9038aba56f8..cdcb8114a3d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -165,7 +165,7 @@ sealed class JavaPeerProxyData public List UcoConstructors { get; } = new (); /// - /// RegisterNatives registrations (method name, JNI signature, wrapper name). + /// RegisterNatives registrations (method name, JNI signature, wrapper target). /// public List NativeRegistrations { get; } = new (); } @@ -232,6 +232,24 @@ sealed record UcoMethodData public bool UsesExportMethodDispatch => ExportMethodDispatch != null; } +sealed record UcoWrapperTargetData +{ + /// + /// Namespace of the generated proxy type containing the wrapper method. + /// + public required string TypeNamespace { get; init; } + + /// + /// Name of the generated proxy type containing the wrapper method. + /// + public required string TypeName { get; init; } + + /// + /// Name of the UCO wrapper method whose function pointer to register. + /// + public required string MethodName { get; init; } +} + sealed record ExportMethodDispatchData { /// @@ -329,6 +347,13 @@ sealed record NativeRegistrationData /// Name of the UCO wrapper method whose function pointer to register. /// public required string WrapperMethodName { get; init; } + + /// + /// Generated proxy wrapper target to register. This may point at a wrapper + /// emitted for a different proxy when inherited virtual overrides share the + /// same base callback. + /// + public required UcoWrapperTargetData WrapperTarget { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index ee5d087206a..db5b311fc3e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -99,6 +99,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } } + BuildNativeRegistrations (model); + // Compute IgnoresAccessChecksTo from cross-assembly references var referencedAssemblies = new SortedSet (StringComparer.Ordinal); foreach (var proxy in model.ProxyTypes) { @@ -314,7 +316,6 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash if (isAcw) { BuildUcoMethods (peer, proxy); BuildUcoConstructors (peer, proxy); - BuildNativeRegistrations (proxy); } return proxy; @@ -374,31 +375,104 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) } } - static void BuildNativeRegistrations (JavaPeerProxyData proxy) + static void BuildNativeRegistrations (TypeMapAssemblyData model) { - foreach (var uco in proxy.UcoMethods) { - proxy.NativeRegistrations.Add (new NativeRegistrationData { - JniMethodName = uco.CallbackMethodName, - JniSignature = uco.JniSignature, - WrapperMethodName = uco.WrapperName, - }); + var sharedWrapperTargets = new Dictionary (); + foreach (var proxy in model.ProxyTypes) { + foreach (var uco in proxy.UcoMethods) { + if (!CanShareUcoWrapper (proxy, uco)) { + continue; + } + + var reuseKey = CreateUcoWrapperReuseKey (uco); + if (!sharedWrapperTargets.ContainsKey (reuseKey)) { + sharedWrapperTargets.Add (reuseKey, CreateWrapperTarget (proxy, uco.WrapperName)); + } + } } - foreach (var uco in proxy.UcoConstructors) { - string jniName = uco.WrapperName; - int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); - if (ucoSuffix >= 0) { - jniName = jniName.Substring (0, ucoSuffix); + foreach (var proxy in model.ProxyTypes) { + var reusedUcoMethods = new HashSet (); + + foreach (var uco in proxy.UcoMethods) { + var wrapperTarget = CreateWrapperTarget (proxy, uco.WrapperName); + if (CanReuseUcoWrapper (proxy, uco) && + sharedWrapperTargets.TryGetValue (CreateUcoWrapperReuseKey (uco), out var sharedWrapperTarget)) { + wrapperTarget = sharedWrapperTarget; + reusedUcoMethods.Add (uco); + } + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = uco.CallbackMethodName, + JniSignature = uco.JniSignature, + WrapperMethodName = wrapperTarget.MethodName, + WrapperTarget = wrapperTarget, + }); } - proxy.NativeRegistrations.Add (new NativeRegistrationData { - JniMethodName = jniName, - JniSignature = uco.JniSignature, - WrapperMethodName = uco.WrapperName, - }); + if (reusedUcoMethods.Count > 0) { + proxy.UcoMethods.RemoveAll (uco => reusedUcoMethods.Contains (uco)); + } + + foreach (var uco in proxy.UcoConstructors) { + string jniName = uco.WrapperName; + int ucoSuffix = jniName.LastIndexOf ("_uco", StringComparison.Ordinal); + if (ucoSuffix >= 0) { + jniName = jniName.Substring (0, ucoSuffix); + } + + var wrapperTarget = CreateWrapperTarget (proxy, uco.WrapperName); + proxy.NativeRegistrations.Add (new NativeRegistrationData { + JniMethodName = jniName, + JniSignature = uco.JniSignature, + WrapperMethodName = wrapperTarget.MethodName, + WrapperTarget = wrapperTarget, + }); + } } } + static bool CanShareUcoWrapper (JavaPeerProxyData proxy, UcoMethodData uco) + { + return !uco.UsesExportMethodDispatch && + !proxy.IsGenericDefinition && + !uco.CallbackType.ManagedTypeName.Contains ('`') && + string.Equals (uco.CallbackType.ManagedTypeName, proxy.TargetType.ManagedTypeName, StringComparison.Ordinal) && + string.Equals (uco.CallbackType.AssemblyName, proxy.TargetType.AssemblyName, StringComparison.Ordinal); + } + + static bool CanReuseUcoWrapper (JavaPeerProxyData proxy, UcoMethodData uco) + { + return !uco.UsesExportMethodDispatch && + !proxy.IsGenericDefinition && + !uco.CallbackType.ManagedTypeName.Contains ('`') && + (!string.Equals (uco.CallbackType.ManagedTypeName, proxy.TargetType.ManagedTypeName, StringComparison.Ordinal) || + !string.Equals (uco.CallbackType.AssemblyName, proxy.TargetType.AssemblyName, StringComparison.Ordinal)); + } + + static UcoWrapperTargetData CreateWrapperTarget (JavaPeerProxyData proxy, string methodName) + { + return new UcoWrapperTargetData { + TypeNamespace = proxy.Namespace, + TypeName = proxy.TypeName, + MethodName = methodName, + }; + } + + static UcoWrapperReuseKey CreateUcoWrapperReuseKey (UcoMethodData uco) + { + return new UcoWrapperReuseKey ( + uco.CallbackType.ManagedTypeName, + uco.CallbackType.AssemblyName, + uco.CallbackMethodName, + uco.JniSignature); + } + + readonly record struct UcoWrapperReuseKey ( + string CallbackTypeName, + string CallbackAssemblyName, + string CallbackMethodName, + string JniSignature); + static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, string outputAssemblyName, string jniName) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index c624d72ec71..b22220985a8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -203,8 +203,8 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) EmitRankSentinels (model); EmitMemberReferences (); - // Track wrapper method names → handles for RegisterNatives - var wrapperHandles = new Dictionary (); + // Track wrapper targets → handles for RegisterNatives. + var wrapperHandles = new Dictionary (); foreach (var proxy in model.ProxyTypes) { EmitProxyType (proxy, wrapperHandles); @@ -598,7 +598,7 @@ ExportMethodDispatchEmitter GetExportMethodDispatchEmitter () return _exportMethodDispatchEmitter; } - void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) + void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { if (proxy.IsAcw) { // RegisterNatives uses RVA-backed UTF-8 fields under . @@ -693,12 +693,12 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) + Dictionary wrapperHandles) { // Filter to only registrations that have corresponding wrapper methods var registrations = proxy.NativeRegistrations; var validRegs = new List<(NativeRegistrationData Reg, MethodDefinitionHandle Wrapper)> (registrations.Count); foreach (var reg in registrations) { - if (wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { + if (wrapperHandles.TryGetValue (reg.WrapperTarget, out var wrapperHandle)) { validRegs.Add ((reg, wrapperHandle)); } } diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index cb0ab1c00ba..73b9bc5ec10 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -106,6 +106,112 @@ public void DotNetRun (bool isRelease, string typemapImplementation, AndroidRunt Assert.IsTrue (didLaunch, "Activity should have started."); } + [Test] + public void TrimmableTypeMapInheritedVirtualOverrideUsesBaseUco ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) + { + const string expectedLogcatOutput = "UCO_OVERRIDE_REUSE_RESULTS 107:211:1:1"; + + if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "ucoverride")) { + IsRelease = true, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (new [] { DeviceAbi }); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + proj.SetDefaultTargetDevice (); + proj.Sources.Add (new BuildItem.Source ("UcoOverrideTypes.cs") { + TextContent = () => @"using System; +using Android.Runtime; + +namespace UnnamedProject +{ + [Register (""my/app/UcoOverrideBase"")] + public abstract class UcoOverrideBase : Java.Lang.Object + { + public UcoOverrideBase () { } + + protected UcoOverrideBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register (""doWork"", ""()I"", """")] + public abstract int DoWork (); + } + + [Register (""my/app/UcoOverrideOne"")] + public class UcoOverrideOne : UcoOverrideBase + { + readonly int value; + public static int Calls; + + public UcoOverrideOne (int value) + { + this.value = value; + } + + protected UcoOverrideOne (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override int DoWork () + { + Calls++; + return value + 100; + } + } + + [Register (""my/app/UcoOverrideTwo"")] + public class UcoOverrideTwo : UcoOverrideBase + { + readonly int value; + public static int Calls; + + public UcoOverrideTwo (int value) + { + this.value = value; + } + + protected UcoOverrideTwo (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override int DoWork () + { + Calls++; + return value + 200; + } + } +} +", + }); + proj.MainActivity = proj.DefaultMainActivity.Replace ( + "//${AFTER_ONCREATE}", + @"var one = new UcoOverrideOne (7); +var two = new UcoOverrideTwo (11); +int oneResult = InvokeDoWork (one); +int twoResult = InvokeDoWork (two); +Console.WriteLine ($""# UCO_OVERRIDE_REUSE_RESULTS {oneResult}:{twoResult}:{UcoOverrideOne.Calls}:{UcoOverrideTwo.Calls}""); +if (oneResult != 107 || twoResult != 211 || UcoOverrideOne.Calls != 1 || UcoOverrideTwo.Calls != 1) { + throw new InvalidOperationException (""Unexpected UCO override dispatch result.""); +} + +static int InvokeDoWork (Java.Lang.Object instance) +{ + IntPtr klass = global::Android.Runtime.JNIEnv.GetObjectClass (instance.Handle); + IntPtr method = global::Android.Runtime.JNIEnv.GetMethodID (klass, ""doWork"", ""()I""); + try { + return global::Android.Runtime.JNIEnv.CallIntMethod (instance.Handle, method); + } finally { + global::Android.Runtime.JNIEnv.DeleteLocalRef (klass); + } +}"); + using var builder = CreateApkBuilder (); + Assert.True (builder.Install (proj), "Project should have installed."); + RunProjectAndAssert (proj, builder, doNotCleanupOnUpdate: true); + Assert.True (WaitForActivityToStart (proj.PackageName, "MainActivity", + Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), ActivityStartTimeoutInSeconds), "Activity should have started."); + Assert.IsTrue (MonitorAdbLogcat ((line) => line.Contains (expectedLogcatOutput), + Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"), 45), $"Output did not contain {expectedLogcatOutput}!"); + Assert.True (builder.Uninstall (proj), "Project should have uninstalled."); + } + [Test] public void DotNetRunWaitForExit () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 1e25ee19728..d2280496774 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -966,6 +966,40 @@ public void Generate_UcoMethod_UsesDefaultUnmanagedCallersOnlyAttribute () Assert.Equal (new byte [] { 0x01, 0x00, 0x00, 0x00 }, reader.GetBlobBytes (ucoAttr.Value)); } + [Fact] + public void Generate_InheritedVirtualOverride_RegisterNativesUsesBaseUcoMethod () + { + var basePeer = MakeAcwPeer ("my/app/AbstractBase", "MyApp.AbstractBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var derivedPeer = MakeInheritedOverridePeer ("my/app/Concrete", "MyApp.Concrete"); + + using var stream = GenerateAssembly ([basePeer, derivedPeer], "InheritedOverrideUco"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var baseProxy = FindProxyType (reader, "MyApp_AbstractBase_Proxy"); + var derivedProxy = FindProxyType (reader, "MyApp_Concrete_Proxy"); + var baseUcoHandle = FindMethodDefinition (reader, baseProxy, "n_doWork_uco_0"); + Assert.Empty (FindMethodDefinitions (reader, derivedProxy, "n_doWork_uco_0")); + + var derivedRegisterNatives = reader.GetMethodDefinition (FindMethodDefinition (reader, derivedProxy, "RegisterNatives")); + var body = pe.GetMethodBody (derivedRegisterNatives.RelativeVirtualAddress); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + Assert.Contains (MetadataTokens.GetToken (baseUcoHandle), ReadLdftnTokens (ilBytes)); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) @@ -976,6 +1010,62 @@ static MemberReference FindCallbackMemberRef (MetadataReader reader, string meth return refs [0]; } + static TypeDefinition FindProxyType (MetadataReader reader, string typeName) + { + return reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Single (t => + reader.GetString (t.Namespace) == "_TypeMap.Proxies" && + reader.GetString (t.Name) == typeName); + } + + static MethodDefinitionHandle FindMethodDefinition (MetadataReader reader, TypeDefinition type, string methodName) + { + return FindMethodDefinitions (reader, type, methodName).Single (); + } + + static List FindMethodDefinitions (MetadataReader reader, TypeDefinition type, string methodName) + { + return type.GetMethods () + .Where (h => reader.GetString (reader.GetMethodDefinition (h).Name) == methodName) + .ToList (); + } + + static List ReadLdftnTokens (byte [] ilBytes) + { + var tokens = new List (); + for (int i = 0; i < ilBytes.Length - 5; i++) { + if (ilBytes [i] != 0xFE || ilBytes [i + 1] != 0x06) { + continue; + } + + tokens.Add (ilBytes [i + 2] | + (ilBytes [i + 3] << 8) | + (ilBytes [i + 4] << 16) | + (ilBytes [i + 5] << 24)); + } + return tokens; + } + + static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName) + { + return MakeAcwPeer (jniName, managedName, "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + DeclaringTypeName = "MyApp.AbstractBase", + DeclaringAssemblyName = "App", + }, + ], + }; + } + [Theory] [InlineData ("()V", 0)] [InlineData ("(I)V", 1)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 06498daa478..869f2acef93 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1323,6 +1323,140 @@ public void Build_InterfaceWithMarshalMethods_IsNotAcw () Assert.Single (model.ProxyTypes); Assert.False (model.ProxyTypes [0].IsAcw); } + + [Fact] + public void Build_InheritedVirtualOverrides_ReuseBaseUcoMethod () + { + var basePeer = MakeAcwPeer ("my/app/AbstractBase", "MyApp.AbstractBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var derivedOne = MakeInheritedOverridePeer ("my/app/ConcreteOne", "MyApp.ConcreteOne"); + var derivedTwo = MakeInheritedOverridePeer ("my/app/ConcreteTwo", "MyApp.ConcreteTwo"); + + var model = BuildModel ([basePeer, derivedOne, derivedTwo]); + var baseProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.AbstractBase"); + var concreteOneProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.ConcreteOne"); + var concreteTwoProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.ConcreteTwo"); + + Assert.Single (baseProxy.UcoMethods); + Assert.Empty (concreteOneProxy.UcoMethods); + Assert.Empty (concreteTwoProxy.UcoMethods); + + var baseWrapperTarget = baseProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork").WrapperTarget; + Assert.Equal (baseProxy.Namespace, baseWrapperTarget.TypeNamespace); + Assert.Equal (baseProxy.TypeName, baseWrapperTarget.TypeName); + Assert.Equal (baseProxy.UcoMethods [0].WrapperName, baseWrapperTarget.MethodName); + + Assert.Equal (baseWrapperTarget, concreteOneProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork").WrapperTarget); + Assert.Equal (baseWrapperTarget, concreteTwoProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork").WrapperTarget); + } + + [Fact] + public void Build_InheritedVirtualOverride_BaseProxyLater_ReuseBaseUcoMethod () + { + var derived = MakeInheritedOverridePeer ("aaa/app/Concrete", "MyApp.Concrete"); + var basePeer = MakeAcwPeer ("zzz/app/AbstractBase", "MyApp.AbstractBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + + var model = BuildModel ([derived, basePeer]); + var baseProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.AbstractBase"); + var derivedProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.Concrete"); + + Assert.Empty (derivedProxy.UcoMethods); + var baseRegistration = baseProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + var registration = derivedProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + Assert.Equal (baseRegistration.WrapperTarget, registration.WrapperTarget); + } + + [Fact] + public void Build_InheritedVirtualOverride_TargetUnavailable_FallsBackToLocalUcoMethod () + { + var derived = MakeInheritedOverridePeer ("my/app/Concrete", "MyApp.Concrete"); + + var model = BuildModel ([derived]); + var derivedProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.Concrete"); + + Assert.Single (derivedProxy.UcoMethods); + var registration = derivedProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + Assert.Equal (derivedProxy.Namespace, registration.WrapperTarget.TypeNamespace); + Assert.Equal (derivedProxy.TypeName, registration.WrapperTarget.TypeName); + Assert.Equal (derivedProxy.UcoMethods [0].WrapperName, registration.WrapperTarget.MethodName); + } + + [Theory] + [InlineData ("(I)V", false, false)] + [InlineData ("()V", true, false)] + [InlineData ("()V", false, true)] + public void Build_UnsafeInheritedVirtualOverride_FallsBackToLocalUcoMethod (string jniSignature, bool isExport, bool isGeneric) + { + var basePeer = MakeAcwPeer ("my/app/AbstractBase", "MyApp.AbstractBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var derived = MakeInheritedOverridePeer ("my/app/Concrete", isGeneric ? "MyApp.Concrete`1" : "MyApp.Concrete", + jniSignature, isExport, isGeneric); + + var model = BuildModel ([basePeer, derived]); + var derivedProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == derived.ManagedTypeName); + + Assert.Single (derivedProxy.UcoMethods); + var registration = derivedProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + Assert.Equal (derivedProxy.Namespace, registration.WrapperTarget.TypeNamespace); + Assert.Equal (derivedProxy.TypeName, registration.WrapperTarget.TypeName); + Assert.Equal (derivedProxy.UcoMethods [0].WrapperName, registration.WrapperTarget.MethodName); + } + + static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName, + string jniSignature = "()V", bool isExport = false, bool isGeneric = false) + { + return MakeAcwPeer (jniName, managedName, "App") with { + IsGenericDefinition = isGeneric, + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = jniSignature, ManagedMethodName = "DoWork", + DeclaringTypeName = "MyApp.AbstractBase", + DeclaringAssemblyName = "App", + IsExport = isExport, + }, + ], + }; + } } public class UcoConstructors From 6ad9bb6e3665796b7cd2224bca9142c115b7fc58 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 08:48:58 +0200 Subject: [PATCH 2/8] Order UCO wrapper emission for shared registrations 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> --- .../Generator/TypeMapAssemblyEmitter.cs | 48 ++++++++++++++++++- .../TypeMapAssemblyGeneratorTests.cs | 34 +++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index b22220985a8..ba69d22dc80 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -206,7 +206,7 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) // Track wrapper targets → handles for RegisterNatives. var wrapperHandles = new Dictionary (); - foreach (var proxy in model.ProxyTypes) { + foreach (var proxy in OrderProxiesForWrapperTargets (model.ProxyTypes)) { EmitProxyType (proxy, wrapperHandles); } @@ -225,6 +225,52 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); } + static List OrderProxiesForWrapperTargets (IReadOnlyList proxies) + { + var proxyByType = new Dictionary<(string Namespace, string TypeName), JavaPeerProxyData> (); + foreach (var proxy in proxies) { + proxyByType [(proxy.Namespace, proxy.TypeName)] = proxy; + } + + var ordered = new List (proxies.Count); + var states = new Dictionary (); + + foreach (var proxy in proxies) { + Visit (proxy); + } + + return ordered; + + void Visit (JavaPeerProxyData proxy) + { + if (states.TryGetValue (proxy, out int state)) { + if (state == 2) { + return; + } + + // A cycle would indicate invalid wrapper-target data. Avoid recursing + // forever and keep the original relative order for the cyclic edge. + return; + } + + states [proxy] = 1; + + foreach (var registration in proxy.NativeRegistrations) { + var target = registration.WrapperTarget; + if (target.TypeNamespace == proxy.Namespace && target.TypeName == proxy.TypeName) { + continue; + } + + if (proxyByType.TryGetValue ((target.TypeNamespace, target.TypeName), out var targetProxy)) { + Visit (targetProxy); + } + } + + states [proxy] = 2; + ordered.Add (proxy); + } + } + void EmitTypeReferences () { var metadata = _pe.Metadata; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d2280496774..28ad721ce4f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1000,6 +1000,40 @@ public void Generate_InheritedVirtualOverride_RegisterNativesUsesBaseUcoMethod ( Assert.Contains (MetadataTokens.GetToken (baseUcoHandle), ReadLdftnTokens (ilBytes)); } + [Fact] + public void Generate_InheritedVirtualOverride_BaseProxyLater_RegisterNativesUsesBaseUcoMethod () + { + var derivedPeer = MakeInheritedOverridePeer ("aaa/app/Concrete", "MyApp.Concrete"); + var basePeer = MakeAcwPeer ("zzz/app/AbstractBase", "MyApp.AbstractBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + + using var stream = GenerateAssembly ([derivedPeer, basePeer], "InheritedOverrideUcoBaseLater"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var baseProxy = FindProxyType (reader, "MyApp_AbstractBase_Proxy"); + var derivedProxy = FindProxyType (reader, "MyApp_Concrete_Proxy"); + var baseUcoHandle = FindMethodDefinition (reader, baseProxy, "n_doWork_uco_0"); + Assert.Empty (FindMethodDefinitions (reader, derivedProxy, "n_doWork_uco_0")); + + var derivedRegisterNatives = reader.GetMethodDefinition (FindMethodDefinition (reader, derivedProxy, "RegisterNatives")); + var body = pe.GetMethodBody (derivedRegisterNatives.RelativeVirtualAddress); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + Assert.Contains (MetadataTokens.GetToken (baseUcoHandle), ReadLdftnTokens (ilBytes)); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) From 1755415e7191cf07727bc65c2087af914ad55e28 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 09:55:33 +0200 Subject: [PATCH 3/8] Test UCO reuse through intermediate inheritance 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> --- .../TypeMapAssemblyGeneratorTests.cs | 42 ++++++++++++++++++- .../Generator/TypeMapModelBuilderTests.cs | 38 ++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 28ad721ce4f..d0730adb5a4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1034,6 +1034,43 @@ public void Generate_InheritedVirtualOverride_BaseProxyLater_RegisterNativesUses Assert.Contains (MetadataTokens.GetToken (baseUcoHandle), ReadLdftnTokens (ilBytes)); } + [Fact] + public void Generate_InheritedVirtualOverride_ThroughIntermediate_RegisterNativesUsesRootBaseUcoMethod () + { + var rootBasePeer = MakeAcwPeer ("my/app/C", "MyApp.C", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var intermediatePeer = MakeAcwPeer ("my/app/B", "MyApp.B", "App"); + var leafPeer = MakeInheritedOverridePeer ("my/app/A", "MyApp.A", declaringTypeName: "MyApp.C"); + + using var stream = GenerateAssembly ([leafPeer, intermediatePeer, rootBasePeer], "InheritedOverrideUcoIntermediate"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var rootBaseProxy = FindProxyType (reader, "MyApp_C_Proxy"); + var intermediateProxy = FindProxyType (reader, "MyApp_B_Proxy"); + var leafProxy = FindProxyType (reader, "MyApp_A_Proxy"); + var rootBaseUcoHandle = FindMethodDefinition (reader, rootBaseProxy, "n_doWork_uco_0"); + Assert.Empty (FindMethodDefinitions (reader, intermediateProxy, "n_doWork_uco_0")); + Assert.Empty (FindMethodDefinitions (reader, leafProxy, "n_doWork_uco_0")); + + var leafRegisterNatives = reader.GetMethodDefinition (FindMethodDefinition (reader, leafProxy, "RegisterNatives")); + var body = pe.GetMethodBody (leafRegisterNatives.RelativeVirtualAddress); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + Assert.Contains (MetadataTokens.GetToken (rootBaseUcoHandle), ReadLdftnTokens (ilBytes)); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) @@ -1081,7 +1118,8 @@ static List ReadLdftnTokens (byte [] ilBytes) return tokens; } - static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName) + static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName, + string declaringTypeName = "MyApp.AbstractBase") { return MakeAcwPeer (jniName, managedName, "App") with { MarshalMethods = [ @@ -1093,7 +1131,7 @@ static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedNam new MarshalMethodInfo { JniName = "doWork", NativeCallbackName = "n_DoWork", JniSignature = "()V", ManagedMethodName = "DoWork", - DeclaringTypeName = "MyApp.AbstractBase", + DeclaringTypeName = declaringTypeName, DeclaringAssemblyName = "App", }, ], diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 869f2acef93..a39518edbf4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1389,6 +1389,39 @@ public void Build_InheritedVirtualOverride_BaseProxyLater_ReuseBaseUcoMethod () Assert.Equal (baseRegistration.WrapperTarget, registration.WrapperTarget); } + [Fact] + public void Build_InheritedVirtualOverride_ThroughIntermediate_ReuseRootBaseUcoMethod () + { + var rootBase = MakeAcwPeer ("my/app/C", "MyApp.C", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var intermediate = MakeAcwPeer ("my/app/B", "MyApp.B", "App"); + var leaf = MakeInheritedOverridePeer ("my/app/A", "MyApp.A", declaringTypeName: "MyApp.C"); + + var model = BuildModel ([leaf, intermediate, rootBase]); + var rootBaseProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.C"); + var intermediateProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.B"); + var leafProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.A"); + + Assert.Single (rootBaseProxy.UcoMethods); + Assert.Empty (intermediateProxy.UcoMethods); + Assert.Empty (leafProxy.UcoMethods); + + var rootBaseRegistration = rootBaseProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + var leafRegistration = leafProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + Assert.Equal (rootBaseRegistration.WrapperTarget, leafRegistration.WrapperTarget); + } + [Fact] public void Build_InheritedVirtualOverride_TargetUnavailable_FallsBackToLocalUcoMethod () { @@ -1437,7 +1470,8 @@ public void Build_UnsafeInheritedVirtualOverride_FallsBackToLocalUcoMethod (stri } static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName, - string jniSignature = "()V", bool isExport = false, bool isGeneric = false) + string jniSignature = "()V", bool isExport = false, bool isGeneric = false, + string declaringTypeName = "MyApp.AbstractBase") { return MakeAcwPeer (jniName, managedName, "App") with { IsGenericDefinition = isGeneric, @@ -1450,7 +1484,7 @@ static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedNam new MarshalMethodInfo { JniName = "doWork", NativeCallbackName = "n_DoWork", JniSignature = jniSignature, ManagedMethodName = "DoWork", - DeclaringTypeName = "MyApp.AbstractBase", + DeclaringTypeName = declaringTypeName, DeclaringAssemblyName = "App", IsExport = isExport, }, From 62c53ce11b4c11e54aeed6eed4bc44b8deb50c7e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 09:59:35 +0200 Subject: [PATCH 4/8] Test UCO reuse respects callback ownership 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> --- .../TypeMapAssemblyGeneratorTests.cs | 51 +++++++++++++++++++ .../Generator/TypeMapModelBuilderTests.cs | 47 +++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d0730adb5a4..d6f9a25e828 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1071,6 +1071,57 @@ public void Generate_InheritedVirtualOverride_ThroughIntermediate_RegisterNative Assert.Contains (MetadataTokens.GetToken (rootBaseUcoHandle), ReadLdftnTokens (ilBytes)); } + [Fact] + public void Generate_InheritedVirtualOverride_IntermediateCallbackOwner_RegisterNativesUsesIntermediateUcoMethod () + { + var rootBasePeer = MakeAcwPeer ("my/app/C", "MyApp.C", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var intermediatePeer = MakeAcwPeer ("my/app/B", "MyApp.B", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var leafPeer = MakeInheritedOverridePeer ("my/app/A", "MyApp.A", declaringTypeName: "MyApp.B"); + + using var stream = GenerateAssembly ([leafPeer, rootBasePeer, intermediatePeer], "InheritedOverrideUcoIntermediateOwner"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var rootBaseProxy = FindProxyType (reader, "MyApp_C_Proxy"); + var intermediateProxy = FindProxyType (reader, "MyApp_B_Proxy"); + var leafProxy = FindProxyType (reader, "MyApp_A_Proxy"); + var rootBaseUcoHandle = FindMethodDefinition (reader, rootBaseProxy, "n_doWork_uco_0"); + var intermediateUcoHandle = FindMethodDefinition (reader, intermediateProxy, "n_doWork_uco_0"); + Assert.Empty (FindMethodDefinitions (reader, leafProxy, "n_doWork_uco_0")); + + var leafRegisterNatives = reader.GetMethodDefinition (FindMethodDefinition (reader, leafProxy, "RegisterNatives")); + var body = pe.GetMethodBody (leafRegisterNatives.RelativeVirtualAddress); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + var ldftnTokens = ReadLdftnTokens (ilBytes); + Assert.DoesNotContain (MetadataTokens.GetToken (rootBaseUcoHandle), ldftnTokens); + Assert.Contains (MetadataTokens.GetToken (intermediateUcoHandle), ldftnTokens); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index a39518edbf4..c44fa8bf4ae 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -1422,6 +1422,53 @@ public void Build_InheritedVirtualOverride_ThroughIntermediate_ReuseRootBaseUcoM Assert.Equal (rootBaseRegistration.WrapperTarget, leafRegistration.WrapperTarget); } + [Fact] + public void Build_InheritedVirtualOverride_IntermediateCallbackOwner_ReuseIntermediateUcoMethod () + { + var rootBase = MakeAcwPeer ("my/app/C", "MyApp.C", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var intermediate = MakeAcwPeer ("my/app/B", "MyApp.B", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var leaf = MakeInheritedOverridePeer ("my/app/A", "MyApp.A", declaringTypeName: "MyApp.B"); + + var model = BuildModel ([leaf, rootBase, intermediate]); + var rootBaseProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.C"); + var intermediateProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.B"); + var leafProxy = model.ProxyTypes.Single (p => p.TargetType.ManagedTypeName == "MyApp.A"); + + Assert.Single (rootBaseProxy.UcoMethods); + Assert.Single (intermediateProxy.UcoMethods); + Assert.Empty (leafProxy.UcoMethods); + + var rootBaseRegistration = rootBaseProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + var intermediateRegistration = intermediateProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + var leafRegistration = leafProxy.NativeRegistrations.Single (r => r.JniMethodName == "n_DoWork"); + Assert.NotEqual (rootBaseRegistration.WrapperTarget, leafRegistration.WrapperTarget); + Assert.Equal (intermediateRegistration.WrapperTarget, leafRegistration.WrapperTarget); + } + [Fact] public void Build_InheritedVirtualOverride_TargetUnavailable_FallsBackToLocalUcoMethod () { From 1cda79e7939d2197fccd2ad3a840cee11e1e33c0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 10:30:28 +0200 Subject: [PATCH 5/8] Add UCO inherited override regression tests 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> --- .../Tests/InstallAndRunTests.cs | 76 ++++++++++++++++-- .../TypeMapAssemblyGeneratorTests.cs | 79 +++++++++++++++++++ 2 files changed, 150 insertions(+), 5 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 73b9bc5ec10..91b3c5cdffb 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -107,9 +107,9 @@ public void DotNetRun (bool isRelease, string typemapImplementation, AndroidRunt } [Test] - public void TrimmableTypeMapInheritedVirtualOverrideUsesBaseUco ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) + public void TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) { - const string expectedLogcatOutput = "UCO_OVERRIDE_REUSE_RESULTS 107:211:1:1"; + const string expectedLogcatOutput = "UCO_OVERRIDE_REUSE_RESULTS 107:211:1:1:405:1:0"; if (IgnoreUnsupportedConfiguration (runtime, release: true)) { return; @@ -178,6 +178,59 @@ public override int DoWork () return value + 200; } } + + [Register (""my/app/UcoOverrideHiddenBase"")] + public class UcoOverrideHiddenBase : Java.Lang.Object + { + public static int Calls; + + public UcoOverrideHiddenBase () { } + + protected UcoOverrideHiddenBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register (""doHiddenWork"", ""()I"", """")] + public virtual int DoHiddenWork () + { + Calls++; + return 300; + } + } + + [Register (""my/app/UcoOverrideHiddenIntermediate"")] + public class UcoOverrideHiddenIntermediate : UcoOverrideHiddenBase + { + public UcoOverrideHiddenIntermediate () { } + + protected UcoOverrideHiddenIntermediate (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + // Deliberately hide the base virtual slot while reusing the same JNI signature. + [Register (""doHiddenWork"", ""()I"", """")] + public new virtual int DoHiddenWork () + { + return 400; + } + } + + [Register (""my/app/UcoOverrideHiddenLeaf"")] + public class UcoOverrideHiddenLeaf : UcoOverrideHiddenIntermediate + { + readonly int value; + public static new int Calls; + + public UcoOverrideHiddenLeaf (int value) + { + this.value = value; + } + + protected UcoOverrideHiddenLeaf (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register (""doHiddenWork"", ""()I"", """")] + public override int DoHiddenWork () + { + Calls++; + return value + 400; + } + } } ", }); @@ -187,15 +240,28 @@ public override int DoWork () var two = new UcoOverrideTwo (11); int oneResult = InvokeDoWork (one); int twoResult = InvokeDoWork (two); -Console.WriteLine ($""# UCO_OVERRIDE_REUSE_RESULTS {oneResult}:{twoResult}:{UcoOverrideOne.Calls}:{UcoOverrideTwo.Calls}""); -if (oneResult != 107 || twoResult != 211 || UcoOverrideOne.Calls != 1 || UcoOverrideTwo.Calls != 1) { +var leaf = new UcoOverrideHiddenLeaf (5); +int leafResult = InvokeDoHiddenWork (leaf); +Console.WriteLine ($""# UCO_OVERRIDE_REUSE_RESULTS {oneResult}:{twoResult}:{UcoOverrideOne.Calls}:{UcoOverrideTwo.Calls}:{leafResult}:{UcoOverrideHiddenLeaf.Calls}:{UcoOverrideHiddenBase.Calls}""); +if (oneResult != 107 || twoResult != 211 || UcoOverrideOne.Calls != 1 || UcoOverrideTwo.Calls != 1 || + leafResult != 405 || UcoOverrideHiddenLeaf.Calls != 1 || UcoOverrideHiddenBase.Calls != 0) { throw new InvalidOperationException (""Unexpected UCO override dispatch result.""); } static int InvokeDoWork (Java.Lang.Object instance) +{ + return InvokeIntMethod (instance, ""doWork""); +} + +static int InvokeDoHiddenWork (Java.Lang.Object instance) +{ + return InvokeIntMethod (instance, ""doHiddenWork""); +} + +static int InvokeIntMethod (Java.Lang.Object instance, string methodName) { IntPtr klass = global::Android.Runtime.JNIEnv.GetObjectClass (instance.Handle); - IntPtr method = global::Android.Runtime.JNIEnv.GetMethodID (klass, ""doWork"", ""()I""); + IntPtr method = global::Android.Runtime.JNIEnv.GetMethodID (klass, methodName, ""()I""); try { return global::Android.Runtime.JNIEnv.CallIntMethod (instance.Handle, method); } finally { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d6f9a25e828..f4418f89281 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1122,6 +1122,52 @@ public void Generate_InheritedVirtualOverride_IntermediateCallbackOwner_Register Assert.Contains (MetadataTokens.GetToken (intermediateUcoHandle), ldftnTokens); } + [Fact] + public void Generate_InheritedVirtualOverride_MissingIntermediateProxy_LocalUcoMethodCallsIntermediateCallback () + { + var rootBasePeer = MakeAcwPeer ("my/app/RootBase", "MyApp.RootBase", "App") with { + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "", NativeCallbackName = "n_ctor", + JniSignature = "()V", ManagedMethodName = ".ctor", + IsConstructor = true, + }, + new MarshalMethodInfo { + JniName = "doWork", NativeCallbackName = "n_DoWork", + JniSignature = "()V", ManagedMethodName = "DoWork", + }, + ], + }; + var leafPeer = MakeInheritedOverridePeer ("my/app/Leaf", "MyApp.Leaf", declaringTypeName: "MyApp.HiddenIntermediate"); + + using var stream = GenerateAssembly ([leafPeer, rootBasePeer], "InheritedOverrideMissingIntermediate"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var rootBaseProxy = FindProxyType (reader, "MyApp_RootBase_Proxy"); + var leafProxy = FindProxyType (reader, "MyApp_Leaf_Proxy"); + var rootBaseUcoHandle = FindMethodDefinition (reader, rootBaseProxy, "n_doWork_uco_0"); + var leafUcoHandle = FindMethodDefinition (reader, leafProxy, "n_doWork_uco_0"); + var rootBaseCallback = FindCallbackMemberRef (reader, "n_DoWork", "MyApp", "RootBase"); + var intermediateCallback = FindCallbackMemberRef (reader, "n_DoWork", "MyApp", "HiddenIntermediate"); + + var leafUco = reader.GetMethodDefinition (leafUcoHandle); + var leafUcoBody = pe.GetMethodBody (leafUco.RelativeVirtualAddress); + var leafUcoBytes = leafUcoBody.GetILBytes (); + Assert.NotNull (leafUcoBytes); + var leafUcoCallTokens = ReadCallTokens (leafUcoBytes); + Assert.DoesNotContain (MetadataTokens.GetToken (rootBaseCallback), leafUcoCallTokens); + Assert.Contains (MetadataTokens.GetToken (intermediateCallback), leafUcoCallTokens); + + var leafRegisterNatives = reader.GetMethodDefinition (FindMethodDefinition (reader, leafProxy, "RegisterNatives")); + var registerBody = pe.GetMethodBody (leafRegisterNatives.RelativeVirtualAddress); + var registerBytes = registerBody.GetILBytes (); + Assert.NotNull (registerBytes); + var ldftnTokens = ReadLdftnTokens (registerBytes); + Assert.DoesNotContain (MetadataTokens.GetToken (rootBaseUcoHandle), ldftnTokens); + Assert.Contains (MetadataTokens.GetToken (leafUcoHandle), ldftnTokens); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) @@ -1132,6 +1178,23 @@ static MemberReference FindCallbackMemberRef (MetadataReader reader, string meth return refs [0]; } + static MemberReferenceHandle FindCallbackMemberRef (MetadataReader reader, string methodName, string parentNamespace, string parentName) + { + var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (MetadataTokens.MemberReferenceHandle) + .Where (h => { + var member = reader.GetMemberReference (h); + if (reader.GetString (member.Name) != methodName || member.Parent.Kind != HandleKind.TypeReference) + return false; + + var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Parent); + return reader.GetString (parent.Namespace) == parentNamespace && + reader.GetString (parent.Name) == parentName; + }) + .ToList (); + return Assert.Single (refs); + } + static TypeDefinition FindProxyType (MetadataReader reader, string typeName) { return reader.TypeDefinitions @@ -1169,6 +1232,22 @@ static List ReadLdftnTokens (byte [] ilBytes) return tokens; } + static List ReadCallTokens (byte [] ilBytes) + { + var tokens = new List (); + for (int i = 0; i < ilBytes.Length - 4; i++) { + if (ilBytes [i] != 0x28) { + continue; + } + + tokens.Add (ilBytes [i + 1] | + (ilBytes [i + 2] << 8) | + (ilBytes [i + 3] << 16) | + (ilBytes [i + 4] << 24)); + } + return tokens; + } + static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName, string declaringTypeName = "MyApp.AbstractBase") { From 1d42ba00843b2a40bfb59b5613f6ecc68db62e1c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 23 May 2026 10:31:33 +0200 Subject: [PATCH 6/8] Simplify typemap UCO test helpers 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> --- .../TypeMapAssemblyGeneratorTests.cs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index f4418f89281..67e20111c8c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1148,8 +1148,8 @@ public void Generate_InheritedVirtualOverride_MissingIntermediateProxy_LocalUcoM var leafProxy = FindProxyType (reader, "MyApp_Leaf_Proxy"); var rootBaseUcoHandle = FindMethodDefinition (reader, rootBaseProxy, "n_doWork_uco_0"); var leafUcoHandle = FindMethodDefinition (reader, leafProxy, "n_doWork_uco_0"); - var rootBaseCallback = FindCallbackMemberRef (reader, "n_DoWork", "MyApp", "RootBase"); - var intermediateCallback = FindCallbackMemberRef (reader, "n_DoWork", "MyApp", "HiddenIntermediate"); + var rootBaseCallback = FindCallbackMemberRefHandle (reader, "n_DoWork", "MyApp", "RootBase"); + var intermediateCallback = FindCallbackMemberRefHandle (reader, "n_DoWork", "MyApp", "HiddenIntermediate"); var leafUco = reader.GetMethodDefinition (leafUcoHandle); var leafUcoBody = pe.GetMethodBody (leafUco.RelativeVirtualAddress); @@ -1178,7 +1178,7 @@ static MemberReference FindCallbackMemberRef (MetadataReader reader, string meth return refs [0]; } - static MemberReferenceHandle FindCallbackMemberRef (MetadataReader reader, string methodName, string parentNamespace, string parentName) + static MemberReferenceHandle FindCallbackMemberRefHandle (MetadataReader reader, string methodName, string parentNamespace, string parentName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) .Select (MetadataTokens.MemberReferenceHandle) @@ -1218,25 +1218,19 @@ static List FindMethodDefinitions (MetadataReader reader static List ReadLdftnTokens (byte [] ilBytes) { - var tokens = new List (); - for (int i = 0; i < ilBytes.Length - 5; i++) { - if (ilBytes [i] != 0xFE || ilBytes [i + 1] != 0x06) { - continue; - } - - tokens.Add (ilBytes [i + 2] | - (ilBytes [i + 3] << 8) | - (ilBytes [i + 4] << 16) | - (ilBytes [i + 5] << 24)); - } - return tokens; + return ReadInlineMethodTokens (ilBytes, 0xFE, 0x06); } static List ReadCallTokens (byte [] ilBytes) + { + return ReadInlineMethodTokens (ilBytes, 0x28); + } + + static List ReadInlineMethodTokens (byte [] ilBytes, byte opcode) { var tokens = new List (); for (int i = 0; i < ilBytes.Length - 4; i++) { - if (ilBytes [i] != 0x28) { + if (ilBytes [i] != opcode) { continue; } @@ -1248,6 +1242,22 @@ static List ReadCallTokens (byte [] ilBytes) return tokens; } + static List ReadInlineMethodTokens (byte [] ilBytes, byte opcodePrefix, byte opcode) + { + var tokens = new List (); + for (int i = 0; i < ilBytes.Length - 5; i++) { + if (ilBytes [i] != opcodePrefix || ilBytes [i + 1] != opcode) { + continue; + } + + tokens.Add (ilBytes [i + 2] | + (ilBytes [i + 3] << 8) | + (ilBytes [i + 4] << 16) | + (ilBytes [i + 5] << 24)); + } + return tokens; + } + static JavaPeerInfo MakeInheritedOverridePeer (string jniName, string managedName, string declaringTypeName = "MyApp.AbstractBase") { From 70b9f8f2815dd54d09d5dfecc5289e361cdc4323 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 20:04:08 +0000 Subject: [PATCH 7/8] Fix UCO override device test Java method Agent-Logs-Url: https://github.com/dotnet/android/sessions/80eb25cb-843f-4ce1-975d-406fac750920 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Tests/InstallAndRunTests.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 91b3c5cdffb..774b3b20ae6 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -135,8 +135,8 @@ public UcoOverrideBase () { } protected UcoOverrideBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""doWork"", ""()I"", """")] - public abstract int DoWork (); + [Register (""hashCode"", ""()I"", """")] + public abstract override int GetHashCode (); } [Register (""my/app/UcoOverrideOne"")] @@ -152,7 +152,7 @@ public UcoOverrideOne (int value) protected UcoOverrideOne (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - public override int DoWork () + public override int GetHashCode () { Calls++; return value + 100; @@ -172,7 +172,7 @@ public UcoOverrideTwo (int value) protected UcoOverrideTwo (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - public override int DoWork () + public override int GetHashCode () { Calls++; return value + 200; @@ -188,8 +188,8 @@ public UcoOverrideHiddenBase () { } protected UcoOverrideHiddenBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""doHiddenWork"", ""()I"", """")] - public virtual int DoHiddenWork () + [Register (""hashCode"", ""()I"", """")] + public override int GetHashCode () { Calls++; return 300; @@ -204,8 +204,8 @@ public UcoOverrideHiddenIntermediate () { } protected UcoOverrideHiddenIntermediate (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } // Deliberately hide the base virtual slot while reusing the same JNI signature. - [Register (""doHiddenWork"", ""()I"", """")] - public new virtual int DoHiddenWork () + [Register (""hashCode"", ""()I"", """")] + public new virtual int GetHashCode () { return 400; } @@ -224,8 +224,8 @@ public UcoOverrideHiddenLeaf (int value) protected UcoOverrideHiddenLeaf (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""doHiddenWork"", ""()I"", """")] - public override int DoHiddenWork () + [Register (""hashCode"", ""()I"", """")] + public override int GetHashCode () { Calls++; return value + 400; @@ -250,12 +250,12 @@ public override int DoHiddenWork () static int InvokeDoWork (Java.Lang.Object instance) { - return InvokeIntMethod (instance, ""doWork""); + return InvokeIntMethod (instance, ""hashCode""); } static int InvokeDoHiddenWork (Java.Lang.Object instance) { - return InvokeIntMethod (instance, ""doHiddenWork""); + return InvokeIntMethod (instance, ""hashCode""); } static int InvokeIntMethod (Java.Lang.Object instance, string methodName) From ac3235b2bd35e46c8d03191b6b49e572e7b745bd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 22:02:25 +0000 Subject: [PATCH 8/8] Use View override for UCO device test Agent-Logs-Url: https://github.com/dotnet/android/sessions/051e20e7-fc90-4cd2-bf5f-597490e8bfe4 Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --- .../Tests/InstallAndRunTests.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 774b3b20ae6..32906eab047 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -124,19 +124,21 @@ public void TrimmableTypeMapInheritedVirtualOverrideUsesCorrectUco ([Values (And proj.SetDefaultTargetDevice (); proj.Sources.Add (new BuildItem.Source ("UcoOverrideTypes.cs") { TextContent = () => @"using System; +using Android.Content; using Android.Runtime; +using Android.Views; namespace UnnamedProject { [Register (""my/app/UcoOverrideBase"")] - public abstract class UcoOverrideBase : Java.Lang.Object + public abstract class UcoOverrideBase : View { - public UcoOverrideBase () { } + public UcoOverrideBase (Context context) : base (context) { } protected UcoOverrideBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""hashCode"", ""()I"", """")] - public abstract override int GetHashCode (); + [Register (""getSolidColor"", ""()I"", ""GetGetSolidColorHandler"")] + public abstract override int SolidColor { get; } } [Register (""my/app/UcoOverrideOne"")] @@ -145,17 +147,18 @@ public class UcoOverrideOne : UcoOverrideBase readonly int value; public static int Calls; - public UcoOverrideOne (int value) + public UcoOverrideOne (Context context, int value) : base (context) { this.value = value; } protected UcoOverrideOne (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - public override int GetHashCode () - { - Calls++; - return value + 100; + public override int SolidColor { + get { + Calls++; + return value + 100; + } } } @@ -165,49 +168,52 @@ public class UcoOverrideTwo : UcoOverrideBase readonly int value; public static int Calls; - public UcoOverrideTwo (int value) + public UcoOverrideTwo (Context context, int value) : base (context) { this.value = value; } protected UcoOverrideTwo (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - public override int GetHashCode () - { - Calls++; - return value + 200; + public override int SolidColor { + get { + Calls++; + return value + 200; + } } } [Register (""my/app/UcoOverrideHiddenBase"")] - public class UcoOverrideHiddenBase : Java.Lang.Object + public class UcoOverrideHiddenBase : View { public static int Calls; - public UcoOverrideHiddenBase () { } + public UcoOverrideHiddenBase (Context context) : base (context) { } protected UcoOverrideHiddenBase (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""hashCode"", ""()I"", """")] - public override int GetHashCode () - { - Calls++; - return 300; + [Register (""getSolidColor"", ""()I"", ""GetGetSolidColorHandler"")] + public override int SolidColor { + get { + Calls++; + return 300; + } } } [Register (""my/app/UcoOverrideHiddenIntermediate"")] public class UcoOverrideHiddenIntermediate : UcoOverrideHiddenBase { - public UcoOverrideHiddenIntermediate () { } + public UcoOverrideHiddenIntermediate (Context context) : base (context) { } protected UcoOverrideHiddenIntermediate (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } // Deliberately hide the base virtual slot while reusing the same JNI signature. - [Register (""hashCode"", ""()I"", """")] - public new virtual int GetHashCode () - { - return 400; + [Register (""getSolidColor"", ""()I"", ""GetGetSolidColorHandler"")] + public new virtual int SolidColor { + get { + return 400; + } } } @@ -217,18 +223,19 @@ public class UcoOverrideHiddenLeaf : UcoOverrideHiddenIntermediate readonly int value; public static new int Calls; - public UcoOverrideHiddenLeaf (int value) + public UcoOverrideHiddenLeaf (Context context, int value) : base (context) { this.value = value; } protected UcoOverrideHiddenLeaf (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } - [Register (""hashCode"", ""()I"", """")] - public override int GetHashCode () - { - Calls++; - return value + 400; + [Register (""getSolidColor"", ""()I"", ""GetGetSolidColorHandler"")] + public override int SolidColor { + get { + Calls++; + return value + 400; + } } } } @@ -236,11 +243,11 @@ public override int GetHashCode () }); proj.MainActivity = proj.DefaultMainActivity.Replace ( "//${AFTER_ONCREATE}", - @"var one = new UcoOverrideOne (7); -var two = new UcoOverrideTwo (11); + @"var one = new UcoOverrideOne (this, 7); +var two = new UcoOverrideTwo (this, 11); int oneResult = InvokeDoWork (one); int twoResult = InvokeDoWork (two); -var leaf = new UcoOverrideHiddenLeaf (5); +var leaf = new UcoOverrideHiddenLeaf (this, 5); int leafResult = InvokeDoHiddenWork (leaf); Console.WriteLine ($""# UCO_OVERRIDE_REUSE_RESULTS {oneResult}:{twoResult}:{UcoOverrideOne.Calls}:{UcoOverrideTwo.Calls}:{leafResult}:{UcoOverrideHiddenLeaf.Calls}:{UcoOverrideHiddenBase.Calls}""); if (oneResult != 107 || twoResult != 211 || UcoOverrideOne.Calls != 1 || UcoOverrideTwo.Calls != 1 || @@ -250,12 +257,12 @@ public override int GetHashCode () static int InvokeDoWork (Java.Lang.Object instance) { - return InvokeIntMethod (instance, ""hashCode""); + return InvokeIntMethod (instance, ""getSolidColor""); } static int InvokeDoHiddenWork (Java.Lang.Object instance) { - return InvokeIntMethod (instance, ""hashCode""); + return InvokeIntMethod (instance, ""getSolidColor""); } static int InvokeIntMethod (Java.Lang.Object instance, string methodName)