Skip to content

Commit 1275ade

Browse files
authored
[TrimmableTypeMap] Fix [Export] JNI signature mapping for non-primitive parameter types (#11051)
## Problem The scanner's `ManagedTypeToJniDescriptor()` method maps managed types to JNI descriptors for `[Export]` method signatures. It correctly handles primitives (`bool` → `Z`, `int` → `I`, etc.) and `string` → `Ljava/lang/String;`, but **falls back to `Ljava/lang/Object;` for all other types**. This means `[Export]` methods with parameters like `Android.Graphics.Bitmap`, `Android.Views.View`, or any custom Java-bound type get incorrect JNI signatures in the generated JCW, causing `NoSuchMethodError` at runtime. ## Fix `ManagedTypeToJniDescriptor()` now resolves Java-bound types by looking up their `[Register]` attribute via the existing `TryResolveJniObjectDescriptor()` infrastructure (already used by the constructor signature path), falling back to `Ljava/lang/Object;` only for types that truly cannot be resolved. ## Changes Made - **`ManagedTypeToJniDescriptor`** — Added `TryResolveJniObjectDescriptor` call before the `Ljava/lang/Object;` fallback; converted from `static` to instance to access the assembly cache. - **`BuildJniSignatureFromManaged`, `ParseExportAttribute`, `ParseExportFieldAsMethod`, `TryGetMethodRegisterInfo`, `CollectExportField`** — Converted from `static` to instance (transitively need the assembly cache for type resolution). - **`ExportWithJavaBoundParams`** test type — Added with three `[Export]` methods taking `Android.Views.View` parameters. - **`Scan_ExportMethod_ResolvesJavaBoundParameterTypes`** test theory — Verifies correct JNI signatures for `[Export]` methods with non-primitive Java-bound parameter types. - **`ExportFieldTests`** — Updated assertion to expect the correctly resolved type name (`my.app.ExportFieldExample`) instead of the previous incorrect `java.lang.Object` fallback. ## Testing - All 303 TrimmableTypeMap unit tests pass, including 3 new test cases.
1 parent 96d619c commit 1275ade

File tree

4 files changed

+47
-10
lines changed

4 files changed

+47
-10
lines changed

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ List<string> ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem
956956
return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null;
957957
}
958958

959-
static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
959+
bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo)
960960
{
961961
exportInfo = null;
962962
foreach (var caHandle in methodDef.GetCustomAttributes ()) {
@@ -1006,7 +1006,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex
10061006
return null;
10071007
}
10081008

1009-
static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
1009+
(RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
10101010
{
10111011
var value = index.DecodeAttribute (ca);
10121012

@@ -1050,7 +1050,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex
10501050
);
10511051
}
10521052

1053-
static string BuildJniSignatureFromManaged (MethodSignature<string> sig)
1053+
string BuildJniSignatureFromManaged (MethodSignature<string> sig)
10541054
{
10551055
var sb = new System.Text.StringBuilder ();
10561056
sb.Append ('(');
@@ -1067,7 +1067,7 @@ static string BuildJniSignatureFromManaged (MethodSignature<string> sig)
10671067
/// [ExportField] methods use the managed method name as the JNI name and have
10681068
/// a connector of "__export__" (matching legacy CecilImporter behavior).
10691069
/// </summary>
1070-
static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
1070+
(RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index)
10711071
{
10721072
var managedName = index.Reader.GetString (methodDef.Name);
10731073
var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default);
@@ -1080,10 +1080,11 @@ static string BuildJniSignatureFromManaged (MethodSignature<string> sig)
10801080
}
10811081

10821082
/// <summary>
1083-
/// Maps a managed type name to its JNI descriptor. Falls back to
1084-
/// "Ljava/lang/Object;" for unknown types (used by [Export] signature computation).
1083+
/// Maps a managed type name to its JNI descriptor. Resolves Java-bound types
1084+
/// via their [Register] attribute, falling back to "Ljava/lang/Object;" only
1085+
/// for types that cannot be resolved (used by [Export] signature computation).
10851086
/// </summary>
1086-
static string ManagedTypeToJniDescriptor (string managedType)
1087+
string ManagedTypeToJniDescriptor (string managedType)
10871088
{
10881089
var primitive = TryGetPrimitiveJniDescriptor (managedType);
10891090
if (primitive is not null) {
@@ -1094,6 +1095,12 @@ static string ManagedTypeToJniDescriptor (string managedType)
10941095
return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}";
10951096
}
10961097

1098+
// Try to resolve as a Java peer type with [Register]
1099+
var resolved = TryResolveJniObjectDescriptor (managedType);
1100+
if (resolved is not null) {
1101+
return resolved;
1102+
}
1103+
10971104
return "Ljava/lang/Object;";
10981105
}
10991106

@@ -1515,7 +1522,7 @@ static List<JavaConstructorInfo> BuildJavaConstructors (List<MarshalMethodInfo>
15151522
/// Checks a single method for [ExportField] and adds a JavaFieldInfo if found.
15161523
/// Called inline during Pass 1 to avoid a separate iteration.
15171524
/// </summary>
1518-
static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<JavaFieldInfo> fields)
1525+
void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<JavaFieldInfo> fields)
15191526
{
15201527
foreach (var caHandle in methodDef.GetCustomAttributes ()) {
15211528
var ca = index.Reader.GetCustomAttribute (caHandle);

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ public void Scanner_DetectsExportFieldsWithCorrectProperties ()
2020
var staticField = peer.JavaFields.First (f => f.FieldName == "STATIC_INSTANCE");
2121
Assert.True (staticField.IsStatic);
2222
Assert.Equal ("GetInstance", staticField.InitializerMethodName);
23-
// Reference type — mapped via JNI signature, not fallback to java.lang.Object
24-
Assert.Equal ("java.lang.Object", staticField.JavaTypeName);
23+
// Reference type — mapped via JNI signature to the actual Java type
24+
Assert.Equal ("my.app.ExportFieldExample", staticField.JavaTypeName);
2525

2626
var instanceField = peer.JavaFields.First (f => f.FieldName == "VALUE");
2727
Assert.False (instanceField.IsStatic);

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases ()
5353
FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName);
5454
}
5555

56+
[Theory]
57+
[InlineData ("processView", "(Landroid/view/View;)V")]
58+
[InlineData ("handleClick", "(Landroid/view/View;I)Z")]
59+
[InlineData ("getViewName", "(Landroid/view/View;)Ljava/lang/String;")]
60+
public void Scan_ExportMethod_ResolvesJavaBoundParameterTypes (string jniName, string expectedSig)
61+
{
62+
var method = FindFixtureByJavaName ("my/app/ExportWithJavaBoundParams")
63+
.MarshalMethods.FirstOrDefault (m => m.JniName == jniName);
64+
Assert.NotNull (method);
65+
Assert.Equal (expectedSig, method.JniSignature);
66+
Assert.Null (method.Connector);
67+
}
68+
5669
[Theory]
5770
[InlineData ("android/app/Activity", "Android.App.Activity")]
5871
[InlineData ("my/app/SimpleActivity", "Android.App.Activity")]

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,23 @@ public class ExportExample : Java.Lang.Object
284284
public void MyExportedMethod () { }
285285
}
286286

287+
/// <summary>
288+
/// Has [Export] methods with non-primitive Java-bound parameter types.
289+
/// The JCW should resolve parameter types via [Register] instead of falling back to Object.
290+
/// </summary>
291+
[Register ("my/app/ExportWithJavaBoundParams")]
292+
public class ExportWithJavaBoundParams : Java.Lang.Object
293+
{
294+
[Java.Interop.Export ("processView")]
295+
public void ProcessView (Android.Views.View view) { }
296+
297+
[Java.Interop.Export ("handleClick")]
298+
public bool HandleClick (Android.Views.View view, int action) { return false; }
299+
300+
[Java.Interop.Export ("getViewName")]
301+
public string GetViewName (Android.Views.View view) { return ""; }
302+
}
303+
287304
/// <summary>
288305
/// Has [Export] methods with different access modifiers.
289306
/// The JCW should respect the C# visibility for [Export] methods.

0 commit comments

Comments
 (0)