From 1409607bba5c5ad775225e3754ea4a02e6103cef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 21 Apr 2026 07:11:30 +0200 Subject: [PATCH 1/3] [trimmable typemap] Fix UCO boolean return type mismatch causing n_* callback trimming MCW-generated n_* callback methods use sbyte (int8) for JNI boolean returns, but the UCO generator's EncodeClrType() used byte (uint8). In ECMA-335 metadata these are different types, so ILLink could not resolve the cross-assembly member reference from the typemap DLL to n_* methods in Mono.Android.dll, treating them as unreachable and trimming them. At runtime this caused MissingMethodException for methods like IHostnameVerifierInvoker.n_Verify_*, which cascaded into SIGSEGV. Fix: add EncodeClrTypeForCallback() that uses sbyte for JNI boolean, matching MCW convention. Use this for the callback member reference while keeping the UCO wrapper's own signature with byte (correct for JNI ABI). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 27 ++++++++++++++++++- .../Generator/TypeMapAssemblyEmitter.cs | 13 ++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 91670834b31..294bff84cb8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -108,10 +108,35 @@ 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, From f89dbc3b35d3a6d49889ff00707539e6975c6293 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 21 Apr 2026 07:25:45 +0200 Subject: [PATCH 2/3] Add tests for UCO boolean signature encoding Unit tests verifying: - EncodeClrType produces correct ECMA-335 type codes for all JNI primitive kinds - EncodeClrTypeForCallback produces correct codes (sbyte for boolean) - Boolean is the ONLY type where the two encoders differ - Both throw on Void (handled separately by callers) End-to-end regression tests verifying generated assembly metadata: - UCO wrapper with boolean return uses byte, callback MemberRef uses sbyte - UCO wrapper with boolean parameter uses byte, callback MemberRef uses sbyte Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) 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)] From b6f44947784c7284f01239c2ed6761309000aaa6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 21 Apr 2026 07:29:15 +0200 Subject: [PATCH 3/3] Fix duplicate XML doc summary block on EncodeClrType Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 294bff84cb8..c9b620ff18f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -105,13 +105,12 @@ 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). /// + /// + /// JNI boolean (Z) maps to byte (unsigned, blittable for the JNI ABI). + /// public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { switch (kind) {