diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
index afa9ea3c3d9..4d83caa1322 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs
@@ -129,16 +129,22 @@ public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kin
}
///
- /// Encodes a JNI type as its CLR equivalent matching the MCW-generated n_* callback
- /// signatures. JNI boolean (Z) maps to sbyte (matching _JniMarshal_*_B delegates).
- /// Use this when constructing member references to n_* methods.
+ /// Encodes a JNI type as its CLR equivalent for a n_* callback MemberRef, used as the
+ /// fallback when the real n_* method's signature cannot be read from metadata — which
+ /// happens for framework callbacks, because the generator scans the compile-time reference
+ /// assemblies (e.g. Microsoft.Android.Ref) and those strip the private static n_*
+ /// methods. Framework bindings are always produced by the current (post-#1296) generator, so the
+ /// blittable encoding is correct for them: JNI boolean → sbyte, JNI char → ushort.
+ /// When the n_* can be read (NuGet binding implementation assemblies such as AndroidX,
+ /// which may be pre-#1296 and declare bool/char), the captured signature is emitted via
+ /// instead of this fallback.
///
public static void EncodeClrTypeForCallback (SignatureTypeEncoder encoder, JniParamKind kind)
{
switch (kind) {
- case JniParamKind.Boolean: encoder.SByte (); break; // MCW n_* callbacks use sbyte for JNI boolean
+ case JniParamKind.Boolean: encoder.SByte (); break; // blittable modern n_* encoding (post-#1296)
case JniParamKind.Byte: encoder.SByte (); break;
- case JniParamKind.Char: encoder.Char (); break;
+ case JniParamKind.Char: encoder.UInt16 (); break; // blittable modern n_* encoding (post-#1296)
case JniParamKind.Short: encoder.Int16 (); break;
case JniParamKind.Int: encoder.Int32 (); break;
case JniParamKind.Long: encoder.Int64 (); break;
@@ -149,6 +155,60 @@ public static void EncodeClrTypeForCallback (SignatureTypeEncoder encoder, JniPa
}
}
+ ///
+ /// True when a JNI kind maps to a CLR type that the MCW n_* callback may declare as either
+ /// its managed form or a blittable form depending on the generator version that compiled the
+ /// binding: JNI boolean is bool (pre-#1296) or sbyte (post-#1296), and JNI char is
+ /// char or ushort. For these, the callback MemberRef is captured from the real
+ /// n_* method's metadata when available; if it can't be read (framework callbacks live in
+ /// reference assemblies that strip the private static n_*), it falls back to the blittable
+ /// encoding via .
+ ///
+ public static bool IsAmbiguousCallbackKind (JniParamKind kind)
+ => kind == JniParamKind.Boolean || kind == JniParamKind.Char;
+
+ ///
+ /// True when a JNI method signature contains a boolean or char parameter/return — the kinds whose
+ /// n_* callback CLR type is ambiguous across generator versions and must therefore be
+ /// captured from the real method's metadata (see ).
+ ///
+ public static bool HasAmbiguousCallbackType (string jniSignature)
+ {
+ if (IsAmbiguousCallbackKind (ParseReturnType (jniSignature))) {
+ return true;
+ }
+ foreach (var kind in ParseParameterTypes (jniSignature)) {
+ if (IsAmbiguousCallbackKind (kind)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Encodes a captured CLR type-name string (e.g. "System.Boolean", "System.SByte", "System.IntPtr")
+ /// onto a signature, used to emit a callback MemberRef that mirrors the real n_* method.
+ ///
+ public static void EncodeClrTypeName (SignatureTypeEncoder encoder, string clrTypeName)
+ {
+ switch (clrTypeName) {
+ case "System.Boolean": encoder.Boolean (); break;
+ case "System.SByte": encoder.SByte (); break;
+ case "System.Byte": encoder.Byte (); break;
+ case "System.Char": encoder.Char (); break;
+ case "System.UInt16": encoder.UInt16 (); break;
+ case "System.Int16": encoder.Int16 (); break;
+ case "System.Int32": encoder.Int32 (); break;
+ case "System.UInt32": encoder.UInt32 (); break;
+ case "System.Int64": encoder.Int64 (); break;
+ case "System.UInt64": encoder.UInt64 (); break;
+ case "System.Single": encoder.Single (); break;
+ case "System.Double": encoder.Double (); break;
+ case "System.IntPtr": encoder.IntPtr (); break;
+ default: throw new ArgumentException ($"Cannot encode CLR type '{clrTypeName}' in a native callback MemberRef");
+ }
+ }
+
///
/// Validates that a JNI type name has the expected structure (e.g., "com/example/MyClass").
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
index 044a07f01b3..a50274c793d 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs
@@ -327,6 +327,20 @@ sealed record UcoMethodData
///
public required string JniSignature { get; init; }
+ ///
+ /// CLR type-name strings for the real n_* callback's JNI parameters (excluding the leading
+ /// jnienv/self IntPtr pair), captured from metadata. When present, the callback MemberRef is
+ /// emitted to mirror these exactly (correctly distinguishing bool/sbyte and char/ushort). Null
+ /// when the n_* signature was not resolved.
+ ///
+ public IReadOnlyList? CallbackParameterTypeNames { get; init; }
+
+ ///
+ /// CLR return type-name string for the real n_* callback, captured from metadata.
+ /// Null when the n_* signature was not resolved.
+ ///
+ public string? CallbackReturnTypeName { get; init; }
+
///
/// Optional metadata for wrappers that dispatch directly to the managed target
/// instead of forwarding to a generated n_* callback.
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
index 2c457e43149..7c3c76791d4 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -364,6 +364,8 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy)
AssemblyName = !mm.DeclaringAssemblyName.IsNullOrEmpty () ? mm.DeclaringAssemblyName : peer.AssemblyName,
},
JniSignature = mm.JniSignature,
+ CallbackParameterTypeNames = mm.NativeCallbackParameterTypeNames,
+ CallbackReturnTypeName = mm.NativeCallbackReturnTypeName,
ExportMethodDispatch = (mm.IsExport || mm.CallManagedMethodDirectly) ? new ExportMethodDispatchData {
ManagedMethodName = mm.ManagedMethodName,
ParameterTypes = mm.ManagedParameterTypes,
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
index b551349354d..0a8a698a6a4 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs
@@ -1205,14 +1205,38 @@ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco, JavaPeerProxyData proxy
JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]);
});
- // Callback member reference: uses MCW n_* types (sbyte for boolean)
+ // Callback member reference: mirror the real MCW n_* method's signature. JNI boolean and char
+ // are ambiguous (bool/sbyte, char/ushort) across generator versions, so when we could read the
+ // real n_* method's signature (NuGet binding implementation assemblies, e.g. AndroidX) we use it
+ // exactly. When we couldn't — framework callbacks, because the generator scans the compile-time
+ // *reference* assemblies (Microsoft.Android.Ref) which strip the private static n_* methods — we
+ // fall back to the blittable encoding (sbyte/ushort). That is correct for those callbacks because
+ // framework bindings are always produced by the current (post-#1296) generator.
+ var capturedParameterTypeNames = uco.CallbackParameterTypeNames;
+ var capturedReturnTypeName = uco.CallbackReturnTypeName;
+ bool hasCapturedCallbackSignature = capturedParameterTypeNames is not null &&
+ capturedReturnTypeName is not null && capturedParameterTypeNames.Count == jniParams.Count;
+
Action encodeCallbackSig = sig => sig.MethodSignature ().Parameters (paramCount,
- rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind); },
+ rt => {
+ if (isVoid) {
+ rt.Void ();
+ } else if (hasCapturedCallbackSignature && capturedReturnTypeName is not null) {
+ JniSignatureHelper.EncodeClrTypeName (rt.Type (), capturedReturnTypeName);
+ } else {
+ JniSignatureHelper.EncodeClrTypeForCallback (rt.Type (), returnKind);
+ }
+ },
p => {
p.AddParameter ().Type ().IntPtr ();
p.AddParameter ().Type ().IntPtr ();
- for (int j = 0; j < jniParams.Count; j++)
- JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]);
+ for (int j = 0; j < jniParams.Count; j++) {
+ if (hasCapturedCallbackSignature && capturedParameterTypeNames is not null) {
+ JniSignatureHelper.EncodeClrTypeName (p.AddParameter ().Type (), capturedParameterTypeNames [j]);
+ } else {
+ JniSignatureHelper.EncodeClrTypeForCallback (p.AddParameter ().Type (), jniParams [j]);
+ }
+ }
});
var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType);
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 0307ac79638..6b6f4601c41 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -212,6 +212,23 @@ public sealed record MarshalMethodInfo
///
public required string NativeCallbackName { get; init; }
+ ///
+ /// CLR type-name strings (e.g. "System.Boolean", "System.SByte", "System.IntPtr") for the
+ /// real native n_* callback's JNI parameters, in order, excluding the leading
+ /// jnienv/native__this IntPtr pair. Captured from the actual n_* method's
+ /// metadata signature so the emitted callback MemberRef matches it exactly — importantly, JNI
+ /// boolean is bool or sbyte and JNI char is char or ushort depending
+ /// on which generator version compiled the binding (see java-interop #1296). Null when the
+ /// n_* method's signature could not be resolved.
+ ///
+ internal IReadOnlyList? NativeCallbackParameterTypeNames { get; init; }
+
+ ///
+ /// CLR return type-name string for the real native n_* callback, captured from metadata.
+ /// Null when the n_* method's signature could not be resolved.
+ ///
+ internal string? NativeCallbackReturnTypeName { get; init; }
+
///
/// True if this is a constructor registration.
///
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index e184795495a..7eff64e8b8f 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -55,6 +55,103 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan
return false;
}
+ ///
+ /// Resolves the type that declares the native n_* callback. When the [Register] connector
+ /// names a declaring type (e.g. an *Invoker in another assembly) that type is used;
+ /// otherwise the callback lives on the scanned method's own declaring type.
+ ///
+ bool TryResolveNativeCallbackType (MethodDefinition methodDef, AssemblyIndex index,
+ string declaringTypeName, string declaringAssemblyName,
+ [NotNullWhen (true)] out AssemblyIndex? callbackIndex, out TypeDefinitionHandle callbackTypeHandle)
+ {
+ if (!declaringTypeName.IsNullOrEmpty ()) {
+ if (!declaringAssemblyName.IsNullOrEmpty () &&
+ TryResolveType (declaringTypeName, declaringAssemblyName, out callbackTypeHandle, out callbackIndex)) {
+ return true;
+ }
+ // Type-only connector (no assembly), or the named assembly wasn't indexed:
+ // search every indexed assembly for the type by full name.
+ foreach (var candidate in assemblyCache.Values) {
+ if (candidate.TypesByFullName.TryGetValue (declaringTypeName, out callbackTypeHandle)) {
+ callbackIndex = candidate;
+ return true;
+ }
+ }
+ callbackIndex = null;
+ callbackTypeHandle = default;
+ return false;
+ }
+
+ callbackIndex = index;
+ callbackTypeHandle = methodDef.GetDeclaringType ();
+ return true;
+ }
+
+ ///
+ /// Reads the real native n_* callback method's metadata signature so the emitter can
+ /// mirror it exactly. The n_* signature is
+ /// (IntPtr jnienv, IntPtr native__this, <native params...>); the leading IntPtr pair
+ /// is dropped and the remaining parameter type names (plus the return type name) are returned.
+ /// Distinguishing e.g. System.Boolean from System.SByte here is what lets the
+ /// callback MemberRef bind against bindings compiled by either the pre- or post-#1296 generator.
+ ///
+ static bool TryReadNativeCallbackSignature (AssemblyIndex callbackIndex, TypeDefinitionHandle callbackTypeHandle,
+ string nativeCallbackName, int jniParameterCount,
+ [NotNullWhen (true)] out IReadOnlyList? parameterTypeNames, [NotNullWhen (true)] out string? returnTypeName)
+ {
+ parameterTypeNames = null;
+ returnTypeName = null;
+
+ var reader = callbackIndex.Reader;
+ var typeDef = reader.GetTypeDefinition (callbackTypeHandle);
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var methodDef = reader.GetMethodDefinition (methodHandle);
+ if ((methodDef.Attributes & MethodAttributes.Static) == 0) {
+ continue;
+ }
+ if (reader.GetString (methodDef.Name) != nativeCallbackName) {
+ continue;
+ }
+
+ var signature = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null);
+ // n_* callbacks take (IntPtr jnienv, IntPtr native__this) before the native parameters.
+ // Verify both the arity and that leading pair so a same-named static method with a
+ // coincidentally-matching arity can never feed the wrong native param types into the ref.
+ if (signature.ParameterTypes.Length != jniParameterCount + 2) {
+ continue;
+ }
+ if (signature.ParameterTypes [0] != "System.IntPtr" || signature.ParameterTypes [1] != "System.IntPtr") {
+ continue;
+ }
+
+ var names = new string [jniParameterCount];
+ for (int i = 0; i < jniParameterCount; i++) {
+ names [i] = signature.ParameterTypes [i + 2];
+ }
+ parameterTypeNames = names;
+ returnTypeName = signature.ReturnType;
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Captures the real n_* callback signature for a callback declared on
+ /// (used by the base-hierarchy [Register] paths, where the callback always lives on a named base type).
+ ///
+ (IReadOnlyList? ParameterTypeNames, string? ReturnTypeName) CaptureNativeCallbackSignature (
+ TypeRefData declaringType, string nativeCallbackName, string jniSignature)
+ {
+ if (TryResolveType (declaringType.ManagedTypeName, declaringType.AssemblyName, out var handle, out var index)) {
+ int jniParameterCount = JniSignatureHelper.ParseParameterTypes (jniSignature).Count;
+ if (TryReadNativeCallbackSignature (index, handle, nativeCallbackName, jniParameterCount, out var parameterTypeNames, out var returnTypeName)) {
+ return (parameterTypeNames, returnTypeName);
+ }
+ }
+ return (null, null);
+ }
+
///
/// Looks up the [Register] JNI name for a type identified by name + assembly.
///
@@ -1285,12 +1382,18 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
var registerInfo = result.Value.Info;
bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor";
+ string nativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor);
+ var (callbackParameterTypeNames, callbackReturnTypeName) = !isConstructor && JniSignatureHelper.HasAmbiguousCallbackType (registerInfo.Signature)
+ ? CaptureNativeCallbackSignature (result.Value.DeclaringType, nativeCallbackName, registerInfo.Signature)
+ : (null, null);
return new MarshalMethodInfo {
JniName = registerInfo.JniName,
JniSignature = registerInfo.Signature,
Connector = registerInfo.Connector,
ManagedMethodName = methodName,
- NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor),
+ NativeCallbackName = nativeCallbackName,
+ NativeCallbackParameterTypeNames = callbackParameterTypeNames,
+ NativeCallbackReturnTypeName = callbackReturnTypeName,
IsConstructor = isConstructor,
DeclaringTypeName = result.Value.DeclaringType.ManagedTypeName,
DeclaringAssemblyName = result.Value.DeclaringType.AssemblyName,
@@ -1331,12 +1434,18 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con
// Check if the base property has [Register]
var propRegister = TryGetPropertyRegisterInfo (basePropDef, baseIndex);
if (propRegister is not null && propRegister.Signature is not null) {
+ string nativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false);
+ var (callbackParameterTypeNames, callbackReturnTypeName) = JniSignatureHelper.HasAmbiguousCallbackType (propRegister.Signature)
+ ? CaptureNativeCallbackSignature (baseTypeRef, nativeCallbackName, propRegister.Signature)
+ : (null, null);
return new MarshalMethodInfo {
JniName = propRegister.JniName,
JniSignature = propRegister.Signature,
Connector = propRegister.Connector,
ManagedMethodName = getterName,
- NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false),
+ NativeCallbackName = nativeCallbackName,
+ NativeCallbackParameterTypeNames = callbackParameterTypeNames,
+ NativeCallbackReturnTypeName = callbackReturnTypeName,
IsConstructor = false,
DeclaringTypeName = baseTypeRef.ManagedTypeName,
DeclaringAssemblyName = baseTypeRef.AssemblyName,
@@ -1406,6 +1515,24 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf
}
}
+ string nativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor);
+
+ // For methods that forward to a generated static n_* callback, capture the real n_* signature
+ // so the emitted MemberRef mirrors it exactly. This is essential for JNI boolean/char, which
+ // older bindings declare as bool/char and post-#1296 bindings declare as sbyte/ushort. We
+ // capture whenever the signature has an ambiguous (boolean/char) type, regardless of whether
+ // this method currently dispatches directly — a caller may re-target it to n_* forwarding.
+ IReadOnlyList? nativeCallbackParameterTypeNames = null;
+ string? nativeCallbackReturnTypeName = null;
+ if (!isConstructor && !isExport && JniSignatureHelper.HasAmbiguousCallbackType (jniSignature) &&
+ TryResolveNativeCallbackType (methodDef, index, declaringTypeName, declaringAssemblyName, out var callbackIndex, out var callbackTypeHandle)) {
+ int jniParameterCount = JniSignatureHelper.ParseParameterTypes (jniSignature).Count;
+ if (TryReadNativeCallbackSignature (callbackIndex, callbackTypeHandle, nativeCallbackName, jniParameterCount, out var capturedParams, out var capturedReturn)) {
+ nativeCallbackParameterTypeNames = capturedParams;
+ nativeCallbackReturnTypeName = capturedReturn;
+ }
+ }
+
methods.Add (new MarshalMethodInfo {
JniName = registerInfo.JniName,
JniSignature = jniSignature,
@@ -1413,7 +1540,9 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf
ManagedMethodName = managedName,
DeclaringTypeName = declaringTypeName,
DeclaringAssemblyName = declaringAssemblyName,
- NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor),
+ NativeCallbackName = nativeCallbackName,
+ NativeCallbackParameterTypeNames = nativeCallbackParameterTypeNames,
+ NativeCallbackReturnTypeName = nativeCallbackReturnTypeName,
ManagedParameterTypes = managedParameterTypes,
ManagedParameterExportKinds = parameterKinds,
ManagedReturnType = callManagedMethodDirectly ? EnrichTypeRefWithEnumInfo (managedTypeSig.ReturnType) : new TypeRefData {
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
index 44b043e1ae4..c6a75468349 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs
@@ -951,9 +951,9 @@ public void EncodeClrType_ProducesCorrectPrimitiveTypeCode (int kindValue, byte
}
[Theory]
- [InlineData (1, 0x04)] // Boolean → sbyte — matches MCW n_* callbacks
+ [InlineData (1, 0x04)] // Boolean → sbyte (blittable modern n_* fallback)
[InlineData (2, 0x04)] // Byte → sbyte
- [InlineData (3, 0x03)] // Char → char
+ [InlineData (3, 0x07)] // Char → uint16 (blittable modern n_* fallback)
[InlineData (4, 0x06)] // Short → int16
[InlineData (5, 0x08)] // Int → int32
[InlineData (6, 0x0A)] // Long → int64
@@ -962,6 +962,9 @@ public void EncodeClrType_ProducesCorrectPrimitiveTypeCode (int kindValue, byte
[InlineData (9, 0x18)] // Object → IntPtr
public void EncodeClrTypeForCallback_ProducesCorrectPrimitiveTypeCode (int kindValue, byte expectedCode)
{
+ // This is the fallback used when the real n_* method's signature can't be read from metadata
+ // (framework callbacks, whose reference assemblies strip the private static n_*). Framework
+ // bindings are always post-#1296, so boolean → sbyte and char → ushort here.
var kind = (JniParamKind) kindValue;
var blob = new BlobBuilder ();
JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (blob), kind);
@@ -969,19 +972,17 @@ public void EncodeClrTypeForCallback_ProducesCorrectPrimitiveTypeCode (int kindV
}
[Fact]
- public void EncodeClrType_Boolean_DiffersFromCallback ()
+ public void EncodeClrTypeName_EncodesBooleanAndSByteDistinctly ()
{
- var ucoBlob = new BlobBuilder ();
- JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (ucoBlob), JniParamKind.Boolean);
-
- var cbBlob = new BlobBuilder ();
- JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (cbBlob), JniParamKind.Boolean);
+ // The two boolean encodings a real n_* callback can use must map to distinct metadata codes,
+ // so a captured signature is emitted faithfully (0x02 = ELEMENT_TYPE_BOOLEAN, 0x04 = I1/sbyte).
+ var boolBlob = new BlobBuilder ();
+ JniSignatureHelper.EncodeClrTypeName (new SignatureTypeEncoder (boolBlob), "System.Boolean");
+ Assert.Equal (0x02, boolBlob.ToArray () [0]);
- var ucoBytes = ucoBlob.ToArray ();
- var cbBytes = cbBlob.ToArray ();
- Assert.NotEqual (ucoBytes, cbBytes);
- Assert.Equal (0x05, ucoBytes [0]); // byte (unsigned)
- Assert.Equal (0x04, cbBytes [0]); // sbyte (signed)
+ var sbyteBlob = new BlobBuilder ();
+ JniSignatureHelper.EncodeClrTypeName (new SignatureTypeEncoder (sbyteBlob), "System.SByte");
+ Assert.Equal (0x04, sbyteBlob.ToArray () [0]);
}
[Fact]
@@ -1021,11 +1022,12 @@ public void EncodeClrType_NonBooleanTypes_IdenticalToCallback (int kindValue)
}
[Fact]
- public void Generate_UcoMethod_BooleanReturn_WrapperUsesByte_CallbackUsesSByte ()
+ public void Generate_UcoMethod_BooleanReturn_WrapperUsesByte_CallbackUsesBoolean ()
{
- // Regression test: the UCO wrapper must use byte (unsigned, JNI ABI) for boolean,
- // but the callback MemberRef must use sbyte (signed, MCW convention).
- // A mismatch caused ILLink to fail resolving the member reference and trim n_* methods.
+ // The callback MemberRef must mirror the real n_* method's signature. The TouchHandler fixture
+ // models a *pre-#1296* binding whose n_OnTouch declares JNI boolean as System.Boolean, so the
+ // emitted ref must be Boolean — while the UCO entry keeps the blittable byte for the JNI ABI.
+ // (The modern sbyte counterpart is covered by Generate_UcoMethod_BooleanReturn_ModernBinding_*.)
var peer = MakeTouchHandlerCallbackDispatchPeer ();
using var stream = GenerateAssembly (new [] { peer }, "BoolReturnTest");
using var pe = new PEReader (stream);
@@ -1042,13 +1044,15 @@ public void Generate_UcoMethod_BooleanReturn_WrapperUsesByte_CallbackUsesSByte (
// Find the callback MemberRef that the UCO wrapper calls (n_OnTouch on the TouchHandler type)
var callbackRef = FindCallbackMemberRef (reader, "n_OnTouch");
var callbackSig = callbackRef.DecodeMethodSignature (SignatureTypeProvider.Instance, null);
- Assert.Equal ("System.SByte", callbackSig.ReturnType);
+ Assert.Equal ("System.Boolean", callbackSig.ReturnType);
}
[Fact]
- public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesSByte ()
+ public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesBoolean ()
{
- // Regression test: boolean parameters must also use the correct encoding.
+ // Boolean parameters follow the same rule: byte for the blittable UCO ABI, and — because the
+ // TouchHandler fixture models a pre-#1296 binding — System.Boolean for the n_* callback ref
+ // (matched from the real n_OnFocusChange method, not hardcoded).
var peer = MakeTouchHandlerCallbackDispatchPeer ();
using var stream = GenerateAssembly (new [] { peer }, "BoolParamTest");
using var pe = new PEReader (stream);
@@ -1066,7 +1070,174 @@ public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesSByte ()
// Find the callback MemberRef
var callbackRef = FindCallbackMemberRef (reader, "n_OnFocusChange");
var callbackSig = callbackRef.DecodeMethodSignature (SignatureTypeProvider.Instance, null);
- Assert.Equal ("System.SByte", callbackSig.ParameterTypes.Last ());
+ Assert.Equal ("System.Boolean", callbackSig.ParameterTypes.Last ());
+ }
+
+ [Fact]
+ public void Generate_UcoMethod_BooleanReturn_ModernBinding_CallbackUsesSByte ()
+ {
+ // Counterpart to the pre-#1296 TouchHandler tests: the IOnLongClickListenerInvoker fixture
+ // models a *post-#1296* binding whose n_OnLongClick declares JNI boolean as the blittable
+ // sbyte. ImplicitMultiListener inherits that interface callback (no own [Register]) and thus
+ // forwards to the invoker's n_*, so the emitted callback MemberRef must be SByte — proving the
+ // boolean encoding is taken from each real n_* method rather than hardcoded to one form.
+ var peers = ScanFixtures ();
+ var peer = peers.First (p => p.JavaName == "my/app/ImplicitMultiListener");
+ using var stream = GenerateAssembly (new [] { peer }, "ModernBoolReturnTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var ucoMethod = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("onLongClick") &&
+ reader.GetString (m.Name).Contains ("_uco_"));
+ var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.Byte", ucoSig.ReturnType);
+
+ var callbackRefHandle = FindCallbackMemberRefHandle (reader, "n_OnLongClick_Landroid_view_View_", "Android.Views", "IOnLongClickListenerInvoker");
+ var callbackSig = reader.GetMemberReference (callbackRefHandle).DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.SByte", callbackSig.ReturnType);
+ }
+
+ [Fact]
+ public void Generate_MixedBindings_EachCallbackMirrorsItsOwnNativeSignature ()
+ {
+ // A single generated assembly may reference n_* callbacks from bindings compiled by different
+ // generator versions. Here the pre-#1296 TouchHandler (bool) and the post-#1296
+ // IOnLongClickListenerInvoker (sbyte) coexist; each callback MemberRef must independently match
+ // its own real method — bool for one, sbyte for the other.
+ var touchPeer = MakeTouchHandlerCallbackDispatchPeer ();
+ var longClickPeer = ScanFixtures ().First (p => p.JavaName == "my/app/ImplicitMultiListener");
+ using var stream = GenerateAssembly (new [] { touchPeer, longClickPeer }, "MixedBindingsTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var touchSig = FindCallbackMemberRef (reader, "n_OnTouch").DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.Boolean", touchSig.ReturnType);
+
+ var longClickRef = FindCallbackMemberRefHandle (reader, "n_OnLongClick_Landroid_view_View_", "Android.Views", "IOnLongClickListenerInvoker");
+ var longClickSig = reader.GetMemberReference (longClickRef).DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.SByte", longClickSig.ReturnType);
+ }
+
+ [Fact]
+ public void Generate_UnresolvedBooleanCallback_FallsBackToBlittable ()
+ {
+ // When a boolean/char callback's real n_* signature can't be read from metadata — which is the
+ // case for framework callbacks, because the generator scans the compile-time *reference*
+ // assemblies (Microsoft.Android.Ref) that strip the private static n_* methods — the emitter
+ // must fall back to the blittable sbyte/ushort encoding rather than fail the build (see #11802
+ // CI regression: XAGTT7009 on Org.XmlPull.V1.IXmlPullParserInvoker.n_IsEmptyElementTag). That
+ // fallback is correct because framework bindings are always produced by the post-#1296 generator.
+ var peer = ScanFixtures ().First (p => p.JavaName == "my/app/ImplicitMultiListener");
+ var unresolvedPeer = peer with {
+ MarshalMethods = peer.MarshalMethods.Select (m =>
+ m.JniSignature == "(Landroid/view/View;)Z"
+ ? m with { NativeCallbackParameterTypeNames = null, NativeCallbackReturnTypeName = null }
+ : m).ToList (),
+ };
+
+ using var stream = GenerateAssembly (new [] { unresolvedPeer }, "UnresolvedBoolTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var callbackRefHandle = FindCallbackMemberRefHandle (reader, "n_OnLongClick_Landroid_view_View_", "Android.Views", "IOnLongClickListenerInvoker");
+ var callbackSig = reader.GetMemberReference (callbackRefHandle).DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.SByte", callbackSig.ReturnType);
+ }
+
+ [Fact]
+ public void Generate_UnresolvedCharCallback_FallsBackToUInt16 ()
+ {
+ // Char shares the same ambiguity as boolean, so the unresolved fallback must apply to it too:
+ // when the real n_* signature can't be read (framework reference assemblies strip the private
+ // static n_*), the callback MemberRef falls back to the blittable ushort encoding.
+ var peer = ScanFixtures ().First (p => p.JavaName == "my/app/KeyListenerImpl");
+ var unresolvedPeer = peer with {
+ MarshalMethods = peer.MarshalMethods.Select (m =>
+ m.JniSignature == "(C)C"
+ ? m with { NativeCallbackParameterTypeNames = null, NativeCallbackReturnTypeName = null }
+ : m).ToList (),
+ };
+
+ using var stream = GenerateAssembly (new [] { unresolvedPeer }, "UnresolvedCharTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var callbackRefHandle = FindCallbackMemberRefHandle (reader, "n_OnKey_C", "Android.Views", "IOnKeyListenerInvoker");
+ var callbackSig = reader.GetMemberReference (callbackRefHandle).DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.UInt16", callbackSig.ReturnType);
+ Assert.Equal ("System.UInt16", callbackSig.ParameterTypes.Last ());
+ }
+
+ [Fact]
+ public void Generate_UcoMethod_CharReturn_LegacyBinding_CallbackUsesChar ()
+ {
+ // JNI char is ambiguous exactly like boolean: the pre-#1296 TouchHandler declares its
+ // n_GetKeyChar return as System.Char, so the callback MemberRef must be Char while the UCO
+ // entry keeps the blittable ushort JNI ABI.
+ var peer = MakeTouchHandlerCallbackDispatchPeer ();
+ using var stream = GenerateAssembly (new [] { peer }, "CharReturnLegacyTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var ucoMethod = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("getKeyChar") &&
+ reader.GetString (m.Name).Contains ("_uco_"));
+ var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.UInt16", ucoSig.ReturnType);
+
+ var callbackSig = FindCallbackMemberRef (reader, "n_GetKeyChar").DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.Char", callbackSig.ReturnType);
+ }
+
+ [Fact]
+ public void Generate_UcoMethod_CharParam_LegacyBinding_CallbackUsesChar ()
+ {
+ // Char parameters follow the same rule: ushort for the blittable UCO ABI, and — because
+ // TouchHandler models a pre-#1296 binding — System.Char for the n_SetKeyChar callback ref.
+ var peer = MakeTouchHandlerCallbackDispatchPeer ();
+ using var stream = GenerateAssembly (new [] { peer }, "CharParamLegacyTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var ucoMethod = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("setKeyChar") &&
+ reader.GetString (m.Name).Contains ("_uco_"));
+ var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.UInt16", ucoSig.ParameterTypes.Last ());
+
+ var callbackSig = FindCallbackMemberRef (reader, "n_SetKeyChar").DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.Char", callbackSig.ParameterTypes.Last ());
+ }
+
+ [Fact]
+ public void Generate_UcoMethod_Char_ModernBinding_CallbackUsesUInt16 ()
+ {
+ // Counterpart to the char TouchHandler tests: the IOnKeyListenerInvoker fixture models a
+ // *post-#1296* binding whose n_OnKey_C declares JNI char as the blittable ushort.
+ // KeyListenerImpl inherits that interface callback (no own [Register]) and forwards to the
+ // invoker's n_*, so both the char return and char parameter of the emitted MemberRef must be
+ // UInt16 — proving char, like boolean, is taken from each real n_* method rather than hardcoded.
+ var peer = ScanFixtures ().First (p => p.JavaName == "my/app/KeyListenerImpl");
+ using var stream = GenerateAssembly (new [] { peer }, "CharModernTest");
+ using var pe = new PEReader (stream);
+ var reader = pe.GetMetadataReader ();
+
+ var ucoMethod = reader.MethodDefinitions
+ .Select (h => reader.GetMethodDefinition (h))
+ .First (m => reader.GetString (m.Name).Contains ("onKey") &&
+ reader.GetString (m.Name).Contains ("_uco_"));
+ var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.UInt16", ucoSig.ReturnType);
+ Assert.Equal ("System.UInt16", ucoSig.ParameterTypes.Last ());
+
+ var callbackRefHandle = FindCallbackMemberRefHandle (reader, "n_OnKey_C", "Android.Views", "IOnKeyListenerInvoker");
+ var callbackSig = reader.GetMemberReference (callbackRefHandle).DecodeMethodSignature (SignatureTypeProvider.Instance, null);
+ Assert.Equal ("System.UInt16", callbackSig.ReturnType);
+ Assert.Equal ("System.UInt16", callbackSig.ParameterTypes.Last ());
}
[Fact]
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index 76d2cd61e2f..ceedbc96fe6 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -134,6 +134,37 @@ public interface IOnLongClickListener
bool OnLongClick (View v);
}
+ [Register ("android/view/View$OnLongClickListener", DoNotGenerateAcw = true)]
+ internal sealed class IOnLongClickListenerInvoker : Java.Lang.Object, IOnLongClickListener
+ {
+ public IOnLongClickListenerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ public bool OnLongClick (View v) => false;
+
+ // Real MCW-style n_* callback. This invoker models the *post-#1296* generator, where JNI
+ // boolean is declared as the blittable System.SByte. The emitted callback MemberRef must
+ // mirror this (sbyte), even though the pre-#1296 TouchHandler above uses bool.
+ static sbyte n_OnLongClick_Landroid_view_View_ (IntPtr jnienv, IntPtr native__this, IntPtr v) => 0;
+ }
+
+ [Register ("android/view/View$OnKeyListener", "", "Android.Views.IOnKeyListenerInvoker")]
+ public interface IOnKeyListener
+ {
+ [Register ("onKey", "(C)C", "GetOnKey_CHandler:Android.Views.IOnKeyListenerInvoker")]
+ char OnKey (char c);
+ }
+
+ [Register ("android/view/View$OnKeyListener", DoNotGenerateAcw = true)]
+ internal sealed class IOnKeyListenerInvoker : Java.Lang.Object, IOnKeyListener
+ {
+ public IOnKeyListenerInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ public char OnKey (char c) => c;
+
+ // Real MCW-style n_* callback. This invoker models the *post-#1296* generator, where JNI char
+ // is declared as the blittable System.UInt16 (ushort). The emitted callback MemberRef must
+ // mirror this (ushort), unlike the pre-#1296 TouchHandler above which uses char.
+ static ushort n_OnKey_C (IntPtr jnienv, IntPtr native__this, ushort c) => c;
+ }
+
///
/// Interface with a registered property (for testing interface property implementation detection).
///
@@ -362,6 +393,20 @@ public virtual void OnScroll (int x, float y, long timestamp, double velocity) {
[Register ("setItems", "([Ljava/lang/String;)V", "GetSetItemsHandler")]
public virtual void SetItems (string[]? items) { }
+
+ [Register ("getKeyChar", "()C", "GetGetKeyCharHandler")]
+ public virtual char GetKeyChar () => '\0';
+
+ [Register ("setKeyChar", "(C)V", "GetSetKeyCharHandler")]
+ public virtual void SetKeyChar (char c) { }
+
+ // Real MCW-style n_* callbacks. This binding models the *pre-#1296* generator, where JNI
+ // boolean is declared as System.Boolean (bool) and JNI char as System.Char (char). The
+ // emitted callback MemberRefs must mirror this.
+ static bool n_OnTouch (IntPtr jnienv, IntPtr native__this, IntPtr v, int action) => false;
+ static void n_OnFocusChange (IntPtr jnienv, IntPtr native__this, IntPtr v, bool hasFocus) { }
+ static char n_GetKeyChar (IntPtr jnienv, IntPtr native__this) => '\0';
+ static void n_SetKeyChar (IntPtr jnienv, IntPtr native__this, char c) { }
}
// --- Covariant return test types ---
@@ -676,6 +721,18 @@ public void OnClick (Android.Views.View v) { }
public bool OnLongClick (Android.Views.View v) => false;
}
+ ///
+ /// Implements a char-bearing interface without [Register], so its onKey marshal method forwards
+ /// to the post-#1296 invoker's ushort n_* callback (the modern char encoding).
+ ///
+ [Register ("my/app/KeyListenerImpl")]
+ public class KeyListenerImpl : Java.Lang.Object, Android.Views.IOnKeyListener
+ {
+ protected KeyListenerImpl (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+
+ public char OnKey (char c) => c;
+ }
+
///
/// Has one interface method with [Register] and one without.
///