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. ///