From 0196ae641512e61c68e0e618d652f041475d9faa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 21 Apr 2026 11:01:26 +0200 Subject: [PATCH 1/7] Make CoreCLR the default runtime for Debug builds --- .../targets/Microsoft.Android.Sdk.DefaultProperties.targets | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets index 22816880d1b..3acf819495a 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets @@ -21,9 +21,6 @@ <_AndroidFastDeploymentSupported Condition=" Exists ('$(MSBuildThisFileDirectory)../tools/Xamarin.Android.Common.Debugging.targets') ">true <_AndroidFastDeploymentSupported Condition=" '$(_AndroidFastDeploymentSupported)' == '' ">False - - true - + true com.companyname.AndroidTest1 1 1.0 From 01d8b4ed7705f914f1051cf15dee2a1fb03df2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Rozs=C3=ADval?= Date: Tue, 21 Apr 2026 13:24:16 +0200 Subject: [PATCH 3/7] [TrimmableTypeMap] Fix UCO boolean return type mismatch causing n_* callback trimming (#11168) ## Summary Fixes a bug in the trimmable typemap UCO (UnmanagedCallersOnly) wrapper generator where the callback member reference used `byte` (uint8) for JNI boolean returns, but MCW-generated `n_*` callback methods use `sbyte` (int8). ## The Bug In ECMA-335 metadata, `int8` and `uint8` are different types for method signature matching. When ILLink performed its reachability analysis, it could not resolve the cross-assembly member reference from the typemap DLL to `n_*` methods in `Mono.Android.dll` because the return type did not match. ILLink then treated these `n_*` callbacks as unreachable and trimmed them. At runtime, this caused `MissingMethodException` for methods like `IHostnameVerifierInvoker.n_Verify_Ljava_lang_String_Ljavax_net_ssl_SSLSession_`, which cascaded into a SIGSEGV on a background thread. ## The Fix - Added `EncodeClrTypeForCallback()` to `JniSignatureHelper` that uses `sbyte` for JNI boolean, matching the MCW `_JniMarshal_*_B` delegate convention. - Updated `EmitUcoMethod()` in `TypeMapAssemblyEmitter` to use the callback-specific encoding when constructing the member reference to `n_*` methods. - The UCO wrapper's own method signature continues to use `byte` (correct for JNI ABI / `[UnmanagedCallersOnly]`). --- .../Generator/JniSignatureHelper.cs | 28 +++- .../Generator/TypeMapAssemblyEmitter.cs | 13 +- .../TypeMapAssemblyGeneratorTests.cs | 148 ++++++++++++++++++ 3 files changed, 186 insertions(+), 3 deletions(-) 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)] From a858858897ee765676a0d89f3219009f185fe3e0 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Apr 2026 10:40:02 -0500 Subject: [PATCH 4/7] Parameterize DotNetNewAndroidTest for MonoVM and CoreCLR Pass UseMonoRuntime=true/false via build parameters to toggle runtime. For CoreCLR, also pass EmbedAssembliesIntoApk=true as a workaround for MSTest's Assembly.Location requirement with fast-deploy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/InstallAndRunTests.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 77772a2e32e..9c017c1fa58 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2233,7 +2233,9 @@ public void StartAndroidActivityRespectsAndroidDeviceUserId () } [Test] - public void DotNetNewAndroidTest () + [TestCase (AndroidRuntime.MonoVM)] + [TestCase (AndroidRuntime.CoreCLR)] + public void DotNetNewAndroidTest (AndroidRuntime runtime) { var templateName = TestName; var projectDirectory = Path.Combine (Root, "temp", templateName); @@ -2244,12 +2246,21 @@ public void DotNetNewAndroidTest () var dotnet = new DotNetCLI (Path.Combine (projectDirectory, $"{templateName}.csproj")); Assert.IsTrue (dotnet.New ("androidtest"), "`dotnet new androidtest` should succeed"); + bool useMonoRuntime = runtime == AndroidRuntime.MonoVM; + var buildParameters = new List { + $"UseMonoRuntime={useMonoRuntime}", + }; + if (runtime == AndroidRuntime.CoreCLR) { + // TODO: MSTest requires Assembly.Location, which returns empty with fast-deploy on CoreCLR. Remove when fast-deploy is fixed. + buildParameters.Add ("EmbedAssembliesIntoApk=true"); + } + // Build and assert 0 warnings - Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); + Assert.IsTrue (dotnet.Build (parameters: buildParameters.ToArray ()), "`dotnet build` should succeed"); dotnet.AssertHasNoWarnings (); // Run instrumentation via `dotnet run` and capture output - using var process = dotnet.StartRun (waitForExit: true); + using var process = dotnet.StartRun (waitForExit: true, parameters: buildParameters.ToArray ()); var locker = new Lock (); var output = new StringBuilder (); From c6d06ac40bead403ee00e39ab42d693c79582413 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Apr 2026 10:43:39 -0500 Subject: [PATCH 5/7] Revert "Set EmbedAssembliesIntoApk=true in androidtest template" This reverts commit 7d615b0b567993c1b87f7815bbb03de191ef525b. --- src/Microsoft.Android.Templates/androidtest/AndroidTest1.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Microsoft.Android.Templates/androidtest/AndroidTest1.csproj b/src/Microsoft.Android.Templates/androidtest/AndroidTest1.csproj index 992d7cd311d..2e31f4cf8d4 100644 --- a/src/Microsoft.Android.Templates/androidtest/AndroidTest1.csproj +++ b/src/Microsoft.Android.Templates/androidtest/AndroidTest1.csproj @@ -7,8 +7,6 @@ enable enable true - - true com.companyname.AndroidTest1 1 1.0 From cf6d4e142595a3984de2d1ce3c6bb33b937d48a7 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Apr 2026 14:06:38 -0500 Subject: [PATCH 6/7] Fix StartRun parameters need /p: prefix DotNetCLI.Build() adds /p: via GetDefaultCommandLineArgs, but StartRun() passes parameters raw. Without /p:, dotnet run receives bare 'UseMonoRuntime=True' which breaks the command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 9c017c1fa58..447d3a319d0 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2260,7 +2260,8 @@ public void DotNetNewAndroidTest (AndroidRuntime runtime) dotnet.AssertHasNoWarnings (); // Run instrumentation via `dotnet run` and capture output - using var process = dotnet.StartRun (waitForExit: true, parameters: buildParameters.ToArray ()); + var runParameters = buildParameters.Select (p => $"/p:{p}").ToArray (); + using var process = dotnet.StartRun (waitForExit: true, parameters: runParameters); var locker = new Lock (); var output = new StringBuilder (); From e7125be4a7a66ac784e46d69a37dbefdfe017dd6 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 21 Apr 2026 16:45:00 -0500 Subject: [PATCH 7/7] Skip DotNetNewAndroidTest(CoreCLR) until MSTest fixes Assembly.Location MSTest's VSTest bridge uses Assembly.Location to resolve test sources, which returns empty on CoreCLR Android. Assert.Ignore with link to tracking issue. See https://github.com/dotnet/android/issues/11174 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 447d3a319d0..6cf1f0471cb 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2250,9 +2250,9 @@ public void DotNetNewAndroidTest (AndroidRuntime runtime) var buildParameters = new List { $"UseMonoRuntime={useMonoRuntime}", }; + if (runtime == AndroidRuntime.CoreCLR) { - // TODO: MSTest requires Assembly.Location, which returns empty with fast-deploy on CoreCLR. Remove when fast-deploy is fixed. - buildParameters.Add ("EmbedAssembliesIntoApk=true"); + Assert.Ignore ("https://github.com/dotnet/android/issues/11174"); } // Build and assert 0 warnings