diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 91670834b31..c9b620ff18f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -106,12 +106,36 @@ static JniParamKind ParseSingleType (string sig, ref int i) } /// - /// Encodes the CLR type for a JNI parameter kind into a signature type encoder. + /// Encodes a JNI type as its CLR equivalent for [UnmanagedCallersOnly] UCO wrapper signatures. /// + /// + /// JNI boolean (Z) maps to byte (unsigned, blittable for the JNI ABI). + /// public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { switch (kind) { - case JniParamKind.Boolean: encoder.Byte (); break; // JNI jboolean is unsigned byte; must be blittable for UCO + case JniParamKind.Boolean: encoder.Byte (); break; // JNI jboolean is unsigned byte; blittable for UCO + case JniParamKind.Byte: encoder.SByte (); break; + case JniParamKind.Char: encoder.Char (); break; + case JniParamKind.Short: encoder.Int16 (); break; + case JniParamKind.Int: encoder.Int32 (); break; + case JniParamKind.Long: encoder.Int64 (); break; + case JniParamKind.Float: encoder.Single (); break; + case JniParamKind.Double: encoder.Double (); break; + case JniParamKind.Object: encoder.IntPtr (); break; + default: throw new ArgumentException ($"Cannot encode JNI param kind {kind} as CLR type"); + } + } + + /// + /// 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. + /// + 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.Byte: encoder.SByte (); break; case JniParamKind.Char: encoder.Char (); break; case JniParamKind.Short: encoder.Int16 (); break; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 423b27102a8..7640479913d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -705,6 +705,7 @@ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco) int paramCount = 2 + jniParams.Count; bool isVoid = returnKind == JniParamKind.Void; + // UCO wrapper signature: uses JNI ABI types (byte for boolean) Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, rt => { if (isVoid) rt.Void (); else JniSignatureHelper.EncodeClrType (rt.Type (), returnKind); }, p => { @@ -714,8 +715,18 @@ MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco) JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); + // Callback member reference: uses MCW n_* types (sbyte for boolean) + Action encodeCallbackSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => { if (isVoid) rt.Void (); 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]); + }); + var callbackTypeHandle = _pe.ResolveTypeRef (uco.CallbackType); - var callbackRef = _pe.AddMemberRef (callbackTypeHandle, uco.CallbackMethodName, encodeSig); + var callbackRef = _pe.AddMemberRef (callbackTypeHandle, uco.CallbackMethodName, encodeCallbackSig); var handle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 17952f8e1c4..532201e0dcc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -585,6 +585,154 @@ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () Assert.Contains (methodDefs, name => name.Contains ("_uco_")); } + [Theory] + [InlineData (1, 0x05)] // Boolean → byte (unsigned) for JNI ABI + [InlineData (2, 0x04)] // Byte → sbyte + [InlineData (3, 0x03)] // Char → char + [InlineData (4, 0x06)] // Short → int16 + [InlineData (5, 0x08)] // Int → int32 + [InlineData (6, 0x0A)] // Long → int64 + [InlineData (7, 0x0C)] // Float → float32 + [InlineData (8, 0x0D)] // Double → float64 + [InlineData (9, 0x18)] // Object → IntPtr + public void EncodeClrType_ProducesCorrectPrimitiveTypeCode (int kindValue, byte expectedCode) + { + var kind = (JniParamKind) kindValue; + var blob = new BlobBuilder (); + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (blob), kind); + Assert.Equal (expectedCode, blob.ToArray () [0]); + } + + [Theory] + [InlineData (1, 0x04)] // Boolean → sbyte — matches MCW n_* callbacks + [InlineData (2, 0x04)] // Byte → sbyte + [InlineData (3, 0x03)] // Char → char + [InlineData (4, 0x06)] // Short → int16 + [InlineData (5, 0x08)] // Int → int32 + [InlineData (6, 0x0A)] // Long → int64 + [InlineData (7, 0x0C)] // Float → float32 + [InlineData (8, 0x0D)] // Double → float64 + [InlineData (9, 0x18)] // Object → IntPtr + public void EncodeClrTypeForCallback_ProducesCorrectPrimitiveTypeCode (int kindValue, byte expectedCode) + { + var kind = (JniParamKind) kindValue; + var blob = new BlobBuilder (); + JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (blob), kind); + Assert.Equal (expectedCode, blob.ToArray () [0]); + } + + [Fact] + public void EncodeClrType_Boolean_DiffersFromCallback () + { + var ucoBlob = new BlobBuilder (); + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (ucoBlob), JniParamKind.Boolean); + + var cbBlob = new BlobBuilder (); + JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (cbBlob), JniParamKind.Boolean); + + 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) + } + + [Fact] + public void EncodeClrType_Void_Throws () + { + var blob = new BlobBuilder (); + Assert.ThrowsAny (() => + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (blob), JniParamKind.Void)); + } + + [Fact] + public void EncodeClrTypeForCallback_Void_Throws () + { + var blob = new BlobBuilder (); + Assert.ThrowsAny (() => + JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (blob), JniParamKind.Void)); + } + + [Theory] + [InlineData (2)] // Byte + [InlineData (3)] // Char + [InlineData (4)] // Short + [InlineData (5)] // Int + [InlineData (6)] // Long + [InlineData (7)] // Float + [InlineData (8)] // Double + [InlineData (9)] // Object + public void EncodeClrType_NonBooleanTypes_IdenticalToCallback (int kindValue) + { + var kind = (JniParamKind) kindValue; + var ucoBlob = new BlobBuilder (); + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (ucoBlob), kind); + + var cbBlob = new BlobBuilder (); + JniSignatureHelper.EncodeClrTypeForCallback (new SignatureTypeEncoder (cbBlob), kind); + + Assert.Equal (ucoBlob.ToArray (), cbBlob.ToArray ()); + } + + [Fact] + public void Generate_UcoMethod_BooleanReturn_WrapperUsesByte_CallbackUsesSByte () + { + // 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. + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + using var stream = GenerateAssembly (new [] { peer }, "BoolReturnTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Find the UCO wrapper method for onTouch (returns Z → boolean) + var ucoMethod = reader.MethodDefinitions + .Select (h => reader.GetMethodDefinition (h)) + .First (m => reader.GetString (m.Name).Contains ("onTouch") && + reader.GetString (m.Name).Contains ("_uco_")); + var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + Assert.Equal ("System.Byte", ucoSig.ReturnType); + + // 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); + } + + [Fact] + public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesSByte () + { + // Regression test: boolean parameters must also use the correct encoding. + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + using var stream = GenerateAssembly (new [] { peer }, "BoolParamTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Find the UCO wrapper for onFocusChange (takes Z as 3rd param → boolean parameter) + var ucoMethod = reader.MethodDefinitions + .Select (h => reader.GetMethodDefinition (h)) + .First (m => reader.GetString (m.Name).Contains ("onFocusChange") && + reader.GetString (m.Name).Contains ("_uco_")); + var ucoSig = ucoMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + // Params: IntPtr (jnienv), IntPtr (self), IntPtr (View object), byte (boolean) + Assert.Equal ("System.Byte", ucoSig.ParameterTypes.Last ()); + + // Find the callback MemberRef + var callbackRef = FindCallbackMemberRef (reader, "n_OnFocusChange"); + var callbackSig = callbackRef.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + Assert.Equal ("System.SByte", callbackSig.ParameterTypes.Last ()); + } + + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) + { + var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == methodName) + .ToList (); + Assert.Single (refs); + return refs [0]; + } + [Theory] [InlineData ("()V", 0)] [InlineData ("(I)V", 1)]