From 6f64c62c5cbe238abb946557e362e7334fc94bf4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 10:20:41 +0100 Subject: [PATCH 1/3] [TrimmableTypeMap] Fix NativeCallbackName and DeclaringType derivation from Connector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NativeCallbackName was previously set to a simple "n_{managedName}" which produced incorrect names like "n_OnCreate" instead of the correct "n_OnCreate_Landroid_os_Bundle_" that the JNI native method expects. The new GetNativeCallbackName() derives the correct name from the [Register] Connector field (e.g. "GetOnCreate_Landroid_os_Bundle_Handler" → "n_OnCreate_Landroid_os_Bundle_"). Similarly, AddMarshalMethod was not populating DeclaringTypeName and DeclaringAssemblyName for interface implementation methods. The new ParseConnectorDeclaringType() extracts these from the Connector's type qualifier (after the colon). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 71 ++++++++++++++++++- .../Generator/JcwJavaSourceGeneratorTests.cs | 4 +- .../Scanner/OverrideDetectionTests.cs | 2 +- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f81c0b0b87e..546f857bd3a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -773,7 +773,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = registerInfo.Signature, Connector = registerInfo.Connector, ManagedMethodName = methodName, - NativeCallbackName = isConstructor ? "n_ctor" : $"n_{methodName}", + NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor), IsConstructor = isConstructor, DeclaringTypeName = result.Value.DeclaringTypeName, DeclaringAssemblyName = result.Value.DeclaringAssemblyName, @@ -818,7 +818,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, JniSignature = propRegister.Signature, Connector = propRegister.Connector, ManagedMethodName = getterName, - NativeCallbackName = $"n_{getterName}", + NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false), IsConstructor = false, DeclaringTypeName = baseTypeName, DeclaringAssemblyName = baseAssemblyName, @@ -866,12 +866,18 @@ static void AddMarshalMethod (List methods, RegisterInfo regi string managedName = index.Reader.GetString (methodDef.Name); string jniSignature = registerInfo.Signature ?? "()V"; + string declaringTypeName = ""; + string declaringAssemblyName = ""; + ParseConnectorDeclaringType (registerInfo.Connector, out declaringTypeName, out declaringAssemblyName); + methods.Add (new MarshalMethodInfo { JniName = registerInfo.JniName, JniSignature = jniSignature, Connector = registerInfo.Connector, ManagedMethodName = managedName, - NativeCallbackName = isConstructor ? "n_ctor" : $"n_{managedName}", + DeclaringTypeName = declaringTypeName, + DeclaringAssemblyName = declaringAssemblyName, + NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor), IsConstructor = isConstructor, IsExport = isExport, IsInterfaceImplementation = isInterfaceImplementation, @@ -1383,6 +1389,65 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return (typeName, parentJniName, ns); } + /// + /// Derives the native callback method name from a [Register] attribute's Connector field. + /// The Connector may be a simple name like "GetOnCreate_Landroid_os_Bundle_Handler" + /// or a qualified name like "GetOnClick_Landroid_view_View_Handler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, …". + /// In both cases the result is e.g. "n_OnCreate_Landroid_os_Bundle_". + /// Falls back to "n_{managedName}" when the Connector doesn't follow the expected pattern. + /// + static string GetNativeCallbackName (string? connector, string managedName, bool isConstructor) + { + if (isConstructor) { + return "n_ctor"; + } + + if (connector is not null) { + // Strip the optional type qualifier after ':' + int colonIndex = connector.IndexOf (':'); + string handlerName = colonIndex >= 0 ? connector.Substring (0, colonIndex) : connector; + + if (handlerName.StartsWith ("Get", StringComparison.Ordinal) + && handlerName.EndsWith ("Handler", StringComparison.Ordinal)) { + return "n_" + handlerName.Substring (3, handlerName.Length - 3 - "Handler".Length); + } + } + + return $"n_{managedName}"; + } + + /// + /// Parses the type qualifier from a Connector string. + /// Connector format: "GetOnClickHandler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, Version=…". + /// Extracts the managed type name (converting /+ for nested types) and assembly name. + /// + static void ParseConnectorDeclaringType (string? connector, out string declaringTypeName, out string declaringAssemblyName) + { + declaringTypeName = ""; + declaringAssemblyName = ""; + + if (connector is null) { + return; + } + + int colonIndex = connector.IndexOf (':'); + if (colonIndex < 0) { + return; + } + + // After ':' is "TypeName, AssemblyName, Version=…" (assembly-qualified name) + string typeQualified = connector.Substring (colonIndex + 1); + int commaIndex = typeQualified.IndexOf (','); + if (commaIndex < 0) { + return; + } + + declaringTypeName = typeQualified.Substring (0, commaIndex).Trim ().Replace ('/', '+'); + string rest = typeQualified.Substring (commaIndex + 1).Trim (); + int nextComma = rest.IndexOf (','); + declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); + } + static string GetCrc64PackageName (string ns, string assemblyName) { // Only Mono.Android preserves the namespace directly diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 18c6ff7d6b9..78fcb9159e6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -245,8 +245,8 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () var java = GenerateFixture ("my/app/MainActivity"); AssertContainsLine ("@Override\n", java); AssertContainsLine ("public void onCreate (android.os.Bundle p0)\n", java); - AssertContainsLine ("n_OnCreate (p0);\n", java); - AssertContainsLine ("public native void n_OnCreate (android.os.Bundle p0);\n", java); + AssertContainsLine ("n_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 07e005e6482..54f7e0e133a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -15,7 +15,7 @@ public void Override_DetectedWithCorrectRegistration () var peer = FindFixtureByJavaName ("my/app/UserActivity"); var onCreate = peer.MarshalMethods.First (m => m.JniName == "onCreate"); Assert.Equal ("(Landroid/os/Bundle;)V", onCreate.JniSignature); - Assert.Equal ("n_OnCreate", onCreate.NativeCallbackName); + Assert.Equal ("n_OnCreate_Landroid_os_Bundle_", onCreate.NativeCallbackName); Assert.False (onCreate.IsConstructor); Assert.Equal ("GetOnCreate_Landroid_os_Bundle_Handler", onCreate.Connector); Assert.NotNull (peer.ActivationCtor); From 5cbae0cef29f9ce76cb88a4fe15ffefe6ccd04bb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:07:47 +0100 Subject: [PATCH 2/3] Handle non-assembly-qualified connector type names in ParseConnectorDeclaringType ParseConnectorDeclaringType previously returned empty strings when the connector's type qualifier had no assembly name (no comma). This occurs when connectors use the short form like "GetOnClickHandler:Android.Views.IOnClickListenerInvoker". Now the method extracts the type name even without assembly qualification. Also adds DeclaringTypeName assertions in InterfaceMethodDetectionTests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 12 +++++++++--- .../Scanner/InterfaceMethodDetectionTests.cs | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 546f857bd3a..075ed4f9c41 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1418,8 +1418,10 @@ static string GetNativeCallbackName (string? connector, string managedName, bool /// /// Parses the type qualifier from a Connector string. - /// Connector format: "GetOnClickHandler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, Version=…". - /// Extracts the managed type name (converting /+ for nested types) and assembly name. + /// Connector format is either assembly-qualified: + /// "GetOnClickHandler:Android.Views.View/IOnClickListenerInvoker, Mono.Android, Version=…" + /// or type-only: "GetOnClickHandler:Android.Views.IOnClickListenerInvoker". + /// Extracts the managed type name (converting /+ for nested types) and assembly name (if present). /// static void ParseConnectorDeclaringType (string? connector, out string declaringTypeName, out string declaringAssemblyName) { @@ -1435,10 +1437,14 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring return; } - // After ':' is "TypeName, AssemblyName, Version=…" (assembly-qualified name) + // After ':' is typically "TypeName, AssemblyName, Version=…" (assembly-qualified name), + // but some connectors only provide "TypeName" without an assembly. string typeQualified = connector.Substring (colonIndex + 1); int commaIndex = typeQualified.IndexOf (','); + if (commaIndex < 0) { + // No assembly information; treat the whole segment as the type name + declaringTypeName = typeQualified.Trim ().Replace ('/', '+'); return; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs index 9d78510275c..50c41fb2527 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs @@ -18,6 +18,7 @@ public void ImplicitInterfaceImpl_DetectsOnClickWithCorrectSignatureAndConnector var onClick = peer.MarshalMethods.First (m => m.JniName == "onClick"); Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); Assert.Equal ("GetOnClick_Landroid_view_View_Handler:Android.Views.IOnClickListenerInvoker", onClick.Connector); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", onClick.DeclaringTypeName); } [Fact] @@ -54,6 +55,7 @@ public void InterfacePropertyImpl_DetectedWithCorrectSignature () var getName = peer.MarshalMethods.First (m => m.JniName == "getName"); Assert.Equal ("()Ljava/lang/String;", getName.JniSignature); Assert.Equal ("GetGetNameHandler:Android.Views.IHasNameInvoker", getName.Connector); + Assert.Equal ("Android.Views.IHasNameInvoker", getName.DeclaringTypeName); } [Fact] From 7cade6729acda611dc548508b7204715561d823d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 27 Mar 2026 13:23:44 +0100 Subject: [PATCH 3/3] Add test coverage for assembly-qualified connector format Update IHasName's connector to use the full assembly-qualified form and assert both DeclaringTypeName and DeclaringAssemblyName are parsed correctly in InterfacePropertyImpl_DetectedWithCorrectSignature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/InterfaceMethodDetectionTests.cs | 3 ++- .../TestFixtures/TestTypes.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs index 50c41fb2527..fe3aec846d9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/InterfaceMethodDetectionTests.cs @@ -54,8 +54,9 @@ public void InterfacePropertyImpl_DetectedWithCorrectSignature () var peer = FindFixtureByJavaName ("my/app/ImplicitPropertyImpl"); var getName = peer.MarshalMethods.First (m => m.JniName == "getName"); Assert.Equal ("()Ljava/lang/String;", getName.JniSignature); - Assert.Equal ("GetGetNameHandler:Android.Views.IHasNameInvoker", getName.Connector); + Assert.Equal ("GetGetNameHandler:Android.Views.IHasNameInvoker, TestFixtures, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", getName.Connector); Assert.Equal ("Android.Views.IHasNameInvoker", getName.DeclaringTypeName); + Assert.Equal ("TestFixtures", getName.DeclaringAssemblyName); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 196a5261510..a15b12ef758 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -104,7 +104,7 @@ public interface IOnLongClickListener [Register ("android/view/View$IHasName", "", "Android.Views.IHasNameInvoker")] public interface IHasName { - [Register ("getName", "()Ljava/lang/String;", "GetGetNameHandler:Android.Views.IHasNameInvoker")] + [Register ("getName", "()Ljava/lang/String;", "GetGetNameHandler:Android.Views.IHasNameInvoker, TestFixtures, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")] string? Name { get; } }