From 94ddca52ed55a13aea2b4447bb682e145a0d3b6b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:53:59 +0000 Subject: [PATCH 1/2] Initial plan From 768a003708728b79f177d5110dc7458f19a0befe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:16:24 +0000 Subject: [PATCH 2/2] Fix [Export] JNI signature mapping for non-primitive parameter types ManagedTypeToJniDescriptor now resolves Java-bound types via their [Register] attribute using TryResolveJniObjectDescriptor, falling back to Ljava/lang/Object; only for types that cannot be resolved. Methods that call it are converted from static to instance to access the assembly cache for type resolution. Agent-Logs-Url: https://github.com/dotnet/android/sessions/78609547-4f51-479a-aa5a-03a5bb54a0b8 Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 23 ++++++++++++------- .../Generator/ExportFieldTests.cs | 4 ++-- .../Scanner/JavaPeerScannerTests.Behavior.cs | 13 +++++++++++ .../TestFixtures/TestTypes.cs | 17 ++++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 075ed4f9c41..68ab4058aef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -947,7 +947,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { @@ -997,7 +997,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex return null; } - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = index.DecodeAttribute (ca); @@ -1041,7 +1041,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex ); } - static string BuildJniSignatureFromManaged (MethodSignature sig) + string BuildJniSignatureFromManaged (MethodSignature sig) { var sb = new System.Text.StringBuilder (); sb.Append ('('); @@ -1058,7 +1058,7 @@ static string BuildJniSignatureFromManaged (MethodSignature sig) /// [ExportField] methods use the managed method name as the JNI name and have /// a connector of "__export__" (matching legacy CecilImporter behavior). /// - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var managedName = index.Reader.GetString (methodDef.Name); var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); @@ -1071,10 +1071,11 @@ static string BuildJniSignatureFromManaged (MethodSignature sig) } /// - /// Maps a managed type name to its JNI descriptor. Falls back to - /// "Ljava/lang/Object;" for unknown types (used by [Export] signature computation). + /// Maps a managed type name to its JNI descriptor. Resolves Java-bound types + /// via their [Register] attribute, falling back to "Ljava/lang/Object;" only + /// for types that cannot be resolved (used by [Export] signature computation). /// - static string ManagedTypeToJniDescriptor (string managedType) + string ManagedTypeToJniDescriptor (string managedType) { var primitive = TryGetPrimitiveJniDescriptor (managedType); if (primitive is not null) { @@ -1085,6 +1086,12 @@ static string ManagedTypeToJniDescriptor (string managedType) return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; } + // Try to resolve as a Java peer type with [Register] + var resolved = TryResolveJniObjectDescriptor (managedType); + if (resolved is not null) { + return resolved; + } + return "Ljava/lang/Object;"; } @@ -1506,7 +1513,7 @@ static List BuildJavaConstructors (List /// Checks a single method for [ExportField] and adds a JavaFieldInfo if found. /// Called inline during Pass 1 to avoid a separate iteration. /// - static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List fields) + void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List fields) { foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index f3ebbd59126..3c61b9cc765 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -20,8 +20,8 @@ public void Scanner_DetectsExportFieldsWithCorrectProperties () var staticField = peer.JavaFields.First (f => f.FieldName == "STATIC_INSTANCE"); Assert.True (staticField.IsStatic); Assert.Equal ("GetInstance", staticField.InitializerMethodName); - // Reference type — mapped via JNI signature, not fallback to java.lang.Object - Assert.Equal ("java.lang.Object", staticField.JavaTypeName); + // Reference type — mapped via JNI signature to the actual Java type + Assert.Equal ("my.app.ExportFieldExample", staticField.JavaTypeName); var instanceField = peer.JavaFields.First (f => f.FieldName == "VALUE"); Assert.False (instanceField.IsStatic); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 13365887d5f..ec8ba3e7ed0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -53,6 +53,19 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases () FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); } + [Theory] + [InlineData ("processView", "(Landroid/view/View;)V")] + [InlineData ("handleClick", "(Landroid/view/View;I)Z")] + [InlineData ("getViewName", "(Landroid/view/View;)Ljava/lang/String;")] + public void Scan_ExportMethod_ResolvesJavaBoundParameterTypes (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportWithJavaBoundParams") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + Assert.Null (method.Connector); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a15b12ef758..eac273ba746 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -284,6 +284,23 @@ public class ExportExample : Java.Lang.Object public void MyExportedMethod () { } } + /// + /// Has [Export] methods with non-primitive Java-bound parameter types. + /// The JCW should resolve parameter types via [Register] instead of falling back to Object. + /// + [Register ("my/app/ExportWithJavaBoundParams")] + public class ExportWithJavaBoundParams : Java.Lang.Object + { + [Java.Interop.Export ("processView")] + public void ProcessView (Android.Views.View view) { } + + [Java.Interop.Export ("handleClick")] + public bool HandleClick (Android.Views.View view, int action) { return false; } + + [Java.Interop.Export ("getViewName")] + public string GetViewName (Android.Views.View view) { return ""; } + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods.