From dad95dc6c5f3ad09c962f29dc411fe031d6a28a3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:20:19 +0200 Subject: [PATCH 01/58] Default Android type maps to trimmable Reject the legacy llvm-ir selection with a localized error and cover application and library validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/TOC.yml | 2 + .../building-apps/build-properties.md | 6 +-- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4265.md | 25 +++++++++++ ...soft.Android.Sdk.DefaultProperties.targets | 2 +- .../Properties/Resources.resx | 8 +++- .../InvalidConfigTests.cs | 43 +++++++++++++++++++ .../Xamarin.Android.Common.targets | 5 ++- 8 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4265.md diff --git a/Documentation/docs-mobile/TOC.yml b/Documentation/docs-mobile/TOC.yml index 07954b6a74b..63bbe4ae6ef 100644 --- a/Documentation/docs-mobile/TOC.yml +++ b/Documentation/docs-mobile/TOC.yml @@ -348,6 +348,8 @@ href: messages/xa4263.md - name: XA4264 href: messages/xa4264.md + - name: XA4265 + href: messages/xa4265.md - name: XA4301 href: messages/xa4301.md - name: XA4302 diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 1564a283e8f..5c934d03a93 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -1316,10 +1316,10 @@ This property is obsolete and should not be used. ## AndroidTypeMapImplementation An enum-style property that selects the type map implementation. -Valid values are `llvm-ir` and `trimmable`. +The only supported value is `trimmable`. Setting this property to `llvm-ir` +causes error [XA4265](../messages/xa4265.md). -The default value is `trimmable` when `$(PublishAot)` is `true` and `llvm-ir` -otherwise. +The default value is `trimmable` for all Android projects. ## AndroidUseApkSigner diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 34c8a0fa290..f3b0bd1c0ad 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -237,6 +237,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4262](xa4262.md): Type '{type}' has an invalid [Export] SuperArgumentsString value '{value}'. + [XA4263](xa4263.md): The exported member '{member}' has unsupported signature type '{type}'. Use a Java peer type, a supported managed-to-Java mapping, or [ExportParameter]. + [XA4264](xa4264.md): Java callable wrapper XML file '{path}' was not found. ++ [XA4265](xa4265.md): The 'llvm-ir' type map implementation is no longer supported. Remove the AndroidTypeMapImplementation property or set it to 'trimmable'. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4265.md b/Documentation/docs-mobile/messages/xa4265.md new file mode 100644 index 00000000000..1cdcbb9030c --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4265.md @@ -0,0 +1,25 @@ +--- +title: .NET for Android error XA4265 +description: XA4265 error code +ms.date: 09/24/2026 +f1_keywords: + - "XA4265" +--- + +# .NET for Android error XA4265 + +## Example message + +```text +error XA4265: The 'llvm-ir' type map implementation is no longer supported. Remove the AndroidTypeMapImplementation property or set it to 'trimmable'. +``` + +## Issue + +The project sets `AndroidTypeMapImplementation` to `llvm-ir`, which is no longer +supported. + +## Solution + +Remove the `AndroidTypeMapImplementation` property to use the default +`trimmable` type map, or set it to `trimmable` explicitly. 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 bd2eac6c030..5f7f999bcec 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 @@ -45,7 +45,7 @@ class-parse XAJavaInterop1 <_AndroidJcwCodegenTarget Condition=" '$(_AndroidJcwCodegenTarget)' == '' ">XAJavaInterop1 - trimmable + trimmable true true true diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 261facd06c3..960d5c60c97 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1198,8 +1198,8 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS {0} - Java type name from AndroidManifest.xml - Type '{0}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map. To work around this, do not target the trimmable type map (for example, by switching to the 'llvm-ir' type map implementation), and please report this scenario at https://github.com/dotnet/android/issues so the team can evaluate whether to support it. - The following are literal names and should not be translated: JniAddNativeMethodRegistrationAttribute, llvm-ir. + Type '{0}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map. Please report this scenario at https://github.com/dotnet/android/issues so the team can evaluate whether to support it. + The following is a literal name and should not be translated: JniAddNativeMethodRegistrationAttribute. {0} - Fully-qualified managed type name @@ -1278,6 +1278,10 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS The following are literal names and should not be translated: Java, XML. {0} - Full path to the Java callable wrapper XML file + + The 'llvm-ir' type map implementation is no longer supported. Remove the AndroidTypeMapImplementation property or set it to 'trimmable'. + The following are literal names and should not be translated: llvm-ir, AndroidTypeMapImplementation, trimmable. + Command '{0}' failed.\n{1} '{0}' is a failed command name (potentially with path) followed by all the arguments passed to it. {1} is the combined output on the standard error and standard output streams. diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs index b6f614fa73f..9cf62f023ec 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs @@ -64,6 +64,49 @@ public void UnsupportedJcwCodegenTargetIsRejected ( } } + [TestCase (true, false)] + [TestCase (true, true)] + [TestCase (false, false)] + [TestCase (false, true)] + public void TypeMapDefaultsToTrimmable (bool isApplication, bool isRelease) + { + XamarinProject project = isApplication + ? new XamarinAndroidApplicationProject { IsRelease = isRelease } + : new XamarinAndroidLibraryProject { IsRelease = isRelease }; + project.Imports.Add (new Import ("assert-typemap.targets") { + TextContent = () => """ + + + + + + """, + }); + + using var builder = isApplication ? CreateApkBuilder () : CreateDllBuilder (); + builder.Target = "_AssertTypeMapDefault"; + Assert.IsTrue (builder.Build (project), "The default type map should be trimmable for applications and libraries."); + } + + [TestCase (true, "llvm-ir", "XA4265")] + [TestCase (false, "llvm-ir", "XA4265")] + [TestCase (true, "unsupported", "Invalid value for AndroidTypeMapImplementation")] + [TestCase (false, "unsupported", "Invalid value for AndroidTypeMapImplementation")] + public void UnsupportedTypeMapIsRejected (bool isApplication, string typeMapImplementation, string expectedError) + { + XamarinProject project = isApplication + ? new XamarinAndroidApplicationProject () + : new XamarinAndroidLibraryProject (); + project.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + + using var builder = isApplication ? CreateApkBuilder () : CreateDllBuilder (); + builder.Target = "_CheckForInvalidConfigurationAndPlatform"; + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (project), "An unsupported type map should fail validation."); + StringAssertEx.Contains (expectedError, builder.LastBuildOutput); + } + [Test] [TestCase ("RunAOTCompilation")] [TestCase ("EnableLLVM")] diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 7ffd8c9aabe..815c2ce09da 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -3008,8 +3008,11 @@ because xbuild doesn't support framework reference assemblies. + + Text="Invalid value for AndroidTypeMapImplementation: '$(AndroidTypeMapImplementation)'. The only supported value is: trimmable." /> Date: Thu, 24 Sep 2026 15:24:16 +0200 Subject: [PATCH 02/58] Guard app-only typemap targets and validate build entry points Prevent the default from running the app typemap generator in libraries and reject legacy selection before build, publish, and direct generation targets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../InvalidConfigTests.cs | 32 +++++++++++++++++++ .../TrimmableTypeMapBuildTests.cs | 1 - .../Xamarin.Android.Common.targets | 4 +-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs index 9cf62f023ec..b0f8bb450fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs @@ -89,6 +89,15 @@ public void TypeMapDefaultsToTrimmable (bool isApplication, bool isRelease) Assert.IsTrue (builder.Build (project), "The default type map should be trimmable for applications and libraries."); } + [Test] + public void LibraryBuildDoesNotGenerateTypeMap () + { + var project = new XamarinAndroidLibraryProject (); + using var builder = CreateDllBuilder (); + Assert.IsTrue (builder.Build (project), "A library should build with the trimmable type map default."); + FileAssert.DoesNotExist (builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "typemap-assemblies.txt"))); + } + [TestCase (true, "llvm-ir", "XA4265")] [TestCase (false, "llvm-ir", "XA4265")] [TestCase (true, "unsupported", "Invalid value for AndroidTypeMapImplementation")] @@ -107,6 +116,29 @@ public void UnsupportedTypeMapIsRejected (bool isApplication, string typeMapImpl StringAssertEx.Contains (expectedError, builder.LastBuildOutput); } + [TestCase (true, "Build")] + [TestCase (false, "Build")] + [TestCase (true, "Publish")] + [TestCase (false, "Publish")] + [TestCase (true, "_GenerateJavaStubs")] + [TestCase (true, "_PrepareLinking")] + public void LegacyTypeMapIsRejectedBeforeBuildTargets (bool isApplication, string target) + { + XamarinProject project = isApplication + ? new XamarinAndroidApplicationProject () + : new XamarinAndroidLibraryProject (); + project.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); + if (target == "_PrepareLinking") { + project.SetProperty ("PublishTrimmed", "true"); + } + + using var builder = isApplication ? CreateApkBuilder () : CreateDllBuilder (); + builder.Target = target; + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (project), "Legacy type maps should be rejected before generation."); + StringAssertEx.Contains ("error XA4265:", builder.LastBuildOutput); + } + [Test] [TestCase ("RunAOTCompilation")] [TestCase ("EnableLLVM")] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index cf7754c0c1e..50b6febf26a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -30,7 +30,6 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value IsRelease = isRelease, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 815c2ce09da..eb9c6d8d963 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -3007,7 +3007,7 @@ because xbuild doesn't support framework reference assemblies. + BeforeTargets="_CheckForInvalidConfigurationAndPlatform;_GenerateJavaStubs;_PrepareLinking;Build;Publish"> @@ -3021,7 +3021,7 @@ because xbuild doesn't support framework reference assemblies. + Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' and '$(AndroidApplication)' == 'true' " /> From af434dcb73076e7ac388071274d74c41ded11fea Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 08:21:51 +0200 Subject: [PATCH 03/58] Remove legacy LLVM IR type map build pipeline Keep native host compatibility stubs and P/Invoke preservation while removing unreachable typemap and marshal-method generation, rewriting, and linker steps. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-properties.md | 11 +- Documentation/docs-mobile/messages/index.md | 2 +- Documentation/docs-mobile/messages/xa1049.md | 20 +- .../MonoDroid.Tuner/FindJavaObjectsStep.cs | 14 +- .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 74 -- .../Xamarin.Android.Resource.Designer.targets | 6 +- ...oft.Android.Sdk.AssemblyResolution.targets | 1 - .../Microsoft.Android.Sdk.BuildOrder.targets | 1 - ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 455 ------- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 12 +- .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.resx | 4 +- .../Tasks/AssemblyModifierPipeline.cs | 12 - .../Tasks/CollectTypeMapFilesForArchive.cs | 38 - .../Tasks/CreateTypeManagerJava.cs | 80 -- .../Tasks/GenerateACWMap.cs | 57 - .../GenerateAdditionalProviderSources.cs | 104 +- .../Tasks/GenerateEmptyTypemapStub.cs | 10 - .../Tasks/GenerateJavaCallableWrappers.cs | 109 -- .../Tasks/GenerateJavaStubs.cs | 242 ---- .../Tasks/GenerateMainAndroidManifest.cs | 143 --- .../GenerateNativeAotBootstrapSources.cs | 6 +- ...ateNativeAotLibraryLoadAssemblerSources.cs | 2 +- .../GenerateNativeApplicationConfigSources.cs | 9 +- .../GenerateNativeMarshalMethodSources.cs | 196 +-- .../Tasks/GenerateTypeMappings.cs | 177 --- .../Tasks/GetMonoPlatformJar.cs | 43 - .../Tasks/PreTrimmingFixLegacyDesigner.cs | 94 -- .../Tasks/PrepareAbiItems.cs | 5 +- .../Tasks/RemoveRegisterAttribute.cs | 78 -- .../Tasks/RewriteMarshalMethods.cs | 122 -- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 73 +- .../IncrementalBuildTest.cs | 50 +- .../ManifestTest.cs | 2 +- .../MarshalMethodTests.cs | 58 - .../Tasks/LinkerTests.cs | 18 - .../Tasks/LlvmIrGeneratorTests.cs | 33 +- .../Utilities/AssemblyPipeline.cs | 1 - .../Utilities/JCWGenerator.cs | 349 ------ .../Utilities/ManifestDocument.cs | 2 +- .../Utilities/MarshalMethodCecilAdapter.cs | 261 ---- .../MarshalMethodsAssemblyRewriter.cs | 562 --------- .../Utilities/MarshalMethodsClassifier.cs | 1107 ----------------- .../Utilities/MarshalMethodsCollection.cs | 418 ------- .../Utilities/MarshalMethodsHelpers.cs | 61 - .../MarshalMethodsNativeAssemblyGenerator.cs | 80 +- ...rshalMethodsNativeAssemblyGeneratorBase.cs | 919 -------------- .../Utilities/MarshalMethodsState.cs | 43 - .../Utilities/MonoAndroidHelper.cs | 20 +- .../Utilities/NativeCodeGenState.cs | 56 - .../Utilities/NativeTypeMappingData.cs | 67 - ...PreservePinvokesNativeAssemblyGenerator.cs | 19 +- .../Utilities/TypeMapCecilAdapter.cs | 272 ---- .../Utilities/TypeMapGenerator.cs | 355 ------ .../Utilities/TypeMapObjectsXmlFile.cs | 366 ------ .../Utilities/TypeMappingAssemblyGenerator.cs | 16 - ...TypeMappingDebugNativeAssemblyGenerator.cs | 281 ----- ...peMappingReleaseNativeAssemblyGenerator.cs | 486 -------- .../Xamarin.Android.Build.Tasks.csproj | 4 - .../Xamarin.Android.Common.targets | 118 +- .../Tests/InstallAndRunTests.cs | 17 +- .../Tests/MarshalMethodsGCHangTests.cs | 131 -- .../ScannerRunner.cs | 21 +- .../TypeDataBuilder.cs | 10 +- 64 files changed, 178 insertions(+), 8227 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CollectTypeMapFilesForArchive.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GetMonoPlatformJar.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/RemoveRegisterAttribute.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/MarshalMethodTests.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorBase.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsState.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NativeTypeMappingData.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingAssemblyGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingReleaseNativeAssemblyGenerator.cs delete mode 100644 tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 5c934d03a93..5094917572c 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -425,14 +425,9 @@ The default value is `False` when ## AndroidEnableMarshalMethods -A bool property, that determines whether or not LLVM marshal methods are enabled. -LLVM marshal methods are an app startup optimization which uses native entry points -for Java `native` method registration. - -This property is False by default. - -This property cannot be set to `true` when -[`$(PublishReadyToRun)`](#publishreadytorun) is `true`. +A legacy boolean property for LLVM marshal methods. Marshal methods are no +longer supported. Remove this property or set it to `false`; setting it to +`true` produces error XA1049. Added in .NET 8. diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index f3b0bd1c0ad..a9f84c7259c 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -159,7 +159,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA1046](xa1046.md): Attribute '{0}' in element '{1}' has value '{2}' that cannot be parsed as boolean; {3} line {4}. + [XA1047](xa1047.md): Required attribute '{0}' missing from element '{1}'; {2} line {3}. + [XA1048](xa1048.md): '{0}' does not contain an <instrumentation> element. -+ [XA1049](xa1049.md): The 'AndroidEnableMarshalMethods' and 'PublishReadyToRun' MSBuild properties cannot both be set to 'true'. ++ [XA1049](xa1049.md): The 'AndroidEnableMarshalMethods' MSBuild property is no longer supported. + [XA1050](xa1050.md): The 'AndroidR8ObfuscationMode' MSBuild property has an invalid value. ## XA2xxx: Linker diff --git a/Documentation/docs-mobile/messages/xa1049.md b/Documentation/docs-mobile/messages/xa1049.md index 4d617f1abda..4f690ea9c43 100644 --- a/Documentation/docs-mobile/messages/xa1049.md +++ b/Documentation/docs-mobile/messages/xa1049.md @@ -11,31 +11,21 @@ f1_keywords: ## Example messages ```dotnetcli -error XA1049: The 'AndroidEnableMarshalMethods' and 'PublishReadyToRun' MSBuild properties cannot both be set to 'true'. Set one property to 'false'. +error XA1049: The 'AndroidEnableMarshalMethods' MSBuild property is no longer supported. Remove it or set it to 'false'. ``` ## Issue -[`$(AndroidEnableMarshalMethods)`](../building-apps/build-properties.md#androidenablemarshalmethods) -rewrites managed assemblies after trimming, while -[`$(PublishReadyToRun)`](../building-apps/build-properties.md#publishreadytorun) -adds native code to those assemblies. The marshal methods rewriter cannot modify -ReadyToRun assemblies. +The llvm-ir type map and its marshal-method assembly rewriter are no longer +supported. Setting `$(AndroidEnableMarshalMethods)` to `true` cannot enable +marshal methods with the trimmable type map. ## Solution -Disable marshal methods or ReadyToRun in the project file: +Remove the property or set it to `false` in the project file: ```xml false ``` - -Alternatively: - -```xml - - false - -``` diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs index d3e79f011c4..269e8f152b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs @@ -22,8 +22,6 @@ public class FindJavaObjectsStep : BaseStep, IAssemblyModifierPipelineStep public bool ErrorOnCustomJavaObject { get; set; } - public bool UseMarshalMethods { get; set; } - public TaskLoggingHelper Log { get; set; } public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log; @@ -74,11 +72,8 @@ bool ShouldScan (StepContext context) if (!context.IsAndroidAssembly) return false; - // When marshal methods or non-JavaPeerStyle.XAJavaInterop1 are in use we do not want to skip non-user assemblies (such as Mono.Android) - we need to generate JCWs for them during - // application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android - // build and stored in a jar file. - var useMarshalMethods = !context.IsDebug && context.EnableMarshalMethods; - var shouldSkipNonUserAssemblies = !useMarshalMethods && context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1; + // XAJavaInterop1 uses the framework JCWs from the runtime jar. + var shouldSkipNonUserAssemblies = context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1; if (shouldSkipNonUserAssemblies && !context.IsUserAssembly) { Log.LogDebugMessage ($"Skipping assembly '{context.Source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies"); @@ -114,11 +109,6 @@ List ConvertToCallableWrappers (List types) DefaultMonoRuntimeInitialization = "mono.MonoPackageManager.LoadApplication (context);", }; - if (UseMarshalMethods) { - var classifier = new MarshalMethodsClassifier (Context, Context.Resolver, Log); - reader_options.MethodClassifier = new MarshalMethodsCollection (classifier); - } - foreach (var type in types) { var wrapper = CecilImporter.CreateType (type, Context, reader_options); wrappers.Add (wrapper); diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs deleted file mode 100644 index 43a9b8de334..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Utilities; -using Mono.Cecil; -using Mono.Linker.Steps; -using Xamarin.Android.Tasks; - -namespace MonoDroid.Tuner; - -/// -/// Scans an assembly for JLOs that need to be in the typemap and writes them to an XML file. -/// -public class FindTypeMapObjectsStep : BaseStep, IAssemblyModifierPipelineStep -{ - public bool Debug { get; set; } - - public bool ErrorOnCustomJavaObject { get; set; } - - public TaskLoggingHelper Log { get; set; } - - public FindTypeMapObjectsStep (TaskLoggingHelper log) => Log = log; - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - var destinationTypeMapXml = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (context.Destination.ItemSpec); - - // We only care about assemblies that can contains JLOs - if (!context.IsAndroidAssembly) { - Log.LogDebugMessage ($"Skipping assembly '{assembly.Name.Name}' because it is not an Android assembly"); - TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); - return; - } - - var types = ScanForJavaTypes (assembly); - - var xml = new TypeMapObjectsXmlFile { - AssemblyName = assembly.Name.Name, - AssemblyFullName = Debug ? TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name) : null, - AssemblyMvid = assembly.MainModule.Mvid, - }; - - if (Debug) { - var (typeMapDebugSets, foundJniNativeRegistration) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context, needUniqueAssemblies: false); - - xml.JavaToManagedDebugEntries.AddRange (typeMapDebugSets.JavaToManaged); - xml.ManagedToJavaDebugEntries.AddRange (typeMapDebugSets.ManagedToJava); - xml.FoundJniNativeRegistration = foundJniNativeRegistration; - } else { - var genState = TypeMapCecilAdapter.GetReleaseGenerationState (types, Context, out var foundJniNativeRegistration); - xml.ModuleReleaseData = genState.TempModules.SingleOrDefault ().Value; - } - - xml.Export (destinationTypeMapXml, Log); - } - - List ScanForJavaTypes (AssemblyDefinition assembly) - { - var types = new List (); - - var scanner = new XAJavaTypeScanner (Xamarin.Android.Tools.AndroidTargetArch.None, Log, Context) { - ErrorOnCustomJavaObject = ErrorOnCustomJavaObject - }; - - foreach (ModuleDefinition md in assembly.Modules) { - foreach (TypeDefinition td in md.Types) { - scanner.AddJavaType (td, types); - } - } - - return types; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index e19d986acda..ee4169c966b 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -227,10 +227,8 @@ Copyright (C) 2016 Xamarin. All rights reserved. is processed by the ILLink step. If we do not do this then the reference to `netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes. - No TrimmerRootDescriptor is needed: PreTrimmingFixLegacyDesigner rewrites library - assemblies *before* ILLink, replacing designer field loads with calls to the designer - assembly's property getters. ILLink then naturally preserves only the referenced - properties through its mark step, allowing everything else to be trimmed. + The trimmable typemap targets root the designer assembly when trimming is enabled: + resource IDs may be resolved through reflection at runtime. --> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index bb9999a24f1..39b01d08114 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -73,7 +73,6 @@ properties that determine build ordering. _ResolveSatellitePaths; _CreatePackageWorkspace; _LinkAssemblies; - _AfterILLinkAdditionalSteps; _GenerateJavaStubs; _ManifestMerger; _ConvertCustomView; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets deleted file mode 100644 index d257ad442b4..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ /dev/null @@ -1,455 +0,0 @@ - - - - - - - - - - - - - - true - <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag - - <_BeforeAddStaticResources>$(_BeforeAddStaticResources);_GetMonoPlatformJarPath - <_BeforeCompileJava>$(_BeforeCompileJava);_GetMonoPlatformJarPath - <_BeforeCompileToDalvik>$(_BeforeCompileToDalvik);_GetMonoPlatformJarPath - - - - - - - - - <_GenerateJavaStubsDependsOnTargets> - _SetLatestTargetFrameworkVersion; - _PrepareAssemblies; - _PrepareNativeAssemblySources; - $(_AfterPrepareAssemblies); - _GetGenerateJavaStubsInputs; - - - - - - - <_ManifestOutput Condition=" '$(AndroidManifestMerger)' == 'legacy' ">$(IntermediateOutputPath)android\AndroidManifest.xml - <_ManifestOutput Condition=" '$(AndroidManifestMerger)' != 'legacy' ">$(IntermediateOutputPath)AndroidManifest.xml - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' != 'None'">True - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' == 'None'">False - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' != '' ">True - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' == '' ">False - - - <_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - <_ExtraTrimmerArgs Condition=" '$(_EnableSerializationDiscovery)' != 'false' ">--enable-serialization-discovery $(_ExtraTrimmerArgs) - <_ExtraTrimmerArgs Condition=" '$(_AndroidEnableUnusedTypeChecks)' != 'true' ">--disable-opt unusedtypechecks $(_ExtraTrimmerArgs) - - <_TrimmerDumpDependencies Condition=" '$(LinkerDumpDependencies)' == 'true' ">true - <_AndroidLinkerCustomStepAssembly>$(MSBuildThisFileDirectory)..\tools\Microsoft.Android.Sdk.ILLink.dll - <_SystemIOHashingAssemblyPath>$(MSBuildThisFileDirectory)..\tools\System.IO.Hashing.dll - <_ProguardProjectConfiguration Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)proguard\proguard_project_references.cfg - - - - <_TrimmerCustomData Include="AndroidCustomViewMapFile" Value="$(_OuterCustomViewMapFile)" /> - <_TrimmerCustomData Include="SystemIOHashingAssemblyPath" Value="$(_SystemIOHashingAssemblyPath)" /> - - - - - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveSubStepDispatcher" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.MarkJavaObjects" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.PreserveJavaExceptions" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.PreserveApplications" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveRegistrations" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveJavaInterfaces" /> - - - - - - - - - - - - - - - - - - - - - <_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> - - - - - - - - - - - - - - - - - - <_PreTrimmingSwappableItem Include="@(ResolvedFileToPublish)" - Condition=" '%(Extension)' == '.dll' and Exists('$(IntermediateOutputPath)prelink/%(Filename)%(Extension)') " /> - - - - - - - - - - - - - - <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> - <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> - - - - - - - - - - - - - - - - <_TypeMapKind Condition=" '$(AndroidIncludeDebugSymbols)' != 'True' ">mvid - <_TypeMapKind Condition=" '$(AndroidIncludeDebugSymbols)' == 'True' ">strings-asm - - - <_PropertyCacheItems Include="TypeMapKind=$(_TypeMapKind)" /> - <_PropertyCacheItems Include="Deterministic=$(Deterministic)" /> - - - - - - - <_AndroidJarAndDexDirectory>$(_XATargetFrameworkDirectories) - - - - - - - - - - - - - - - - - - <_AddTypeManagerResourcesOutput Condition=" '$(_AndroidUseMarshalMethods)' == 'True' ">$(_AndroidIntermediateJavaSourceDirectory)mono\TypeManager.java - <_AddTypeManagerResourcesOutput Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">$(IntermediateOutputPath)android\bin\mono.android.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink - - - - - <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - - - - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 364be13713a..30c4a2fc948 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -500,14 +500,15 @@ Outputs="@(_BuildTargetAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> + <_TypeMapAssemblySource Include="@(_TypeMapStubAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> + %(_TypeMapStubAbis.Identity) + - - + Debug="$(AndroidIncludeDebugSymbols)" /> @@ -516,10 +517,7 @@ - @@ -204,9 +203,6 @@ - - JavaInteropTypeManager.java - %(Filename)%(Extension) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index eb9c6d8d963..abeecad2ff6 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -55,8 +55,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - @@ -64,9 +62,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - - @@ -82,7 +77,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - @@ -272,7 +266,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidEnablePreloadAssembliesDefault>False $(_AndroidEnablePreloadAssembliesDefault) <_NativeAssemblySourceDir>$(IntermediateOutputPath)android\ - <_SkipJniAddNativeMethodRegistrationAttributeScan Condition=" '$(_SkipJniAddNativeMethodRegistrationAttributeScan)' == '' ">False <_AndroidFastDeployEnvironmentFiles Condition=" '$(_AndroidFastDeployEnvironmentFiles)' == '' And '$(EmbedAssembliesIntoApk)' == 'False' ">True <_AndroidFastDeployEnvironmentFiles Condition=" '$(_AndroidFastDeployEnvironmentFiles)' == '' ">False @@ -336,16 +329,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidDlopenAssemblyStore>true - - False - - False - - <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' == 'True' ">False - <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' != 'True' ">$(AndroidEnableMarshalMethods) - - - @@ -510,7 +493,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. /> $(IntermediateOutputPath)link.flag <_AndroidApkPerAbiFlagFile>$(IntermediateOutputPath)android\bin\apk_per_abi.flag <_AndroidDebugKeyStoreFlag>$(IntermediateOutputPath)android_debug_keystore.flag - <_AdditionalPostLinkerStepsFlag>$(_AndroidStampDirectory)_AdditionalPostLinkerSteps.stamp - <_AfterILLinkOutputDir>$(IntermediateOutputPath)afterlink\ <_AcwMapFile>$(IntermediateOutputPath)acw-map.txt <_CustomViewMapFile>$(IntermediateOutputPath)customview-map.txt $(RootNamespace) @@ -963,7 +944,6 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" /> <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> <_PropertyCacheItems Include="AndroidTypeMapImplementation=$(AndroidTypeMapImplementation)" /> - <_PropertyCacheItems Include="_AndroidUseMarshalMethods=$(_AndroidUseMarshalMethods)" /> <_PropertyCacheItems Include="_AndroidJcwCodegenTarget=$(_AndroidJcwCodegenTarget)" /> <_PropertyCacheItems Include="_AndroidAssemblyStoreCompressionLevel=$(_AndroidAssemblyStoreCompressionLevel)" /> @@ -1366,17 +1346,9 @@ because xbuild doesn't support framework reference assemblies. - - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime.jar - - - + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar - - - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_clr.jar - <_AllResolvedAssemblies Include="@(ResolvedAssemblies)" /> - - $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty - $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty - @@ -1426,7 +1390,6 @@ because xbuild doesn't support framework reference assemblies. ApplicationJavaClass="$(AndroidApplicationJavaClass)" CodeGenerationTarget="$(AndroidCodegenTarget)" Debug="$(AndroidIncludeDebugSymbols)" - EnableMarshalMethods="$(_AndroidUseMarshalMethods)" ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" PackageNamingPolicy="$(AndroidPackageNamingPolicy)" ResolvedAssemblies="@(_AllResolvedAssemblies)" @@ -1446,77 +1409,6 @@ because xbuild doesn't support framework reference assemblies. - - - - - <_AfterILLinkSourceFiles Include="@(ResolvedAssemblies)" - Condition=" '$(_AndroidUseMarshalMethods)' != 'True' or !$([System.String]::Copy('%(ResolvedAssemblies.Filename)').ToLowerInvariant().EndsWith('.resources')) " /> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' != 'True' " Include="@(_AfterILLinkSourceFiles->'$(_AfterILLinkOutputDir)%(DestinationSubPath)')" /> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' == 'True' " Include="@(_AfterILLinkSourceFiles)" /> - <_AndroidScanMarkerFiles Include="@(ResolvedAssemblies->'%(JavaObjectsXmlFile)')" Condition=" '%(ResolvedAssemblies.JavaObjectsXmlFile)' != '' " /> - - - - - - - - - <_AfterILLinkSourceFiles Remove="@(_AfterILLinkSourceFiles)" /> - <_AfterILLinkDestFiles Remove="@(_AfterILLinkDestFiles)" /> - <_AndroidScanMarkerFiles Remove="@(_AndroidScanMarkerFiles)" /> - - - - - - - - - - <_OrigResolvedAssemblies Include="@(ResolvedAssemblies)" /> - - - - <_OrigResolvedUserAssemblies Include="@(ResolvedUserAssemblies)" /> - - - - <_OrigResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> - - - - <_OrigResolvedAssemblies Remove="@(_OrigResolvedAssemblies)" /> - <_OrigResolvedUserAssemblies Remove="@(_OrigResolvedUserAssemblies)" /> - <_OrigResolvedFrameworkAssemblies Remove="@(_OrigResolvedFrameworkAssemblies)" /> - - - @@ -1779,7 +1671,6 @@ because xbuild doesn't support framework reference assemblies. EnablePreloadAssembliesDefault="$(_AndroidEnablePreloadAssembliesDefault)" PackageNamingPolicy="$(AndroidPackageNamingPolicy)" UseAssemblyStore="$(_AndroidUseAssemblyStore)" - EnableMarshalMethods="$(_AndroidUseMarshalMethods)" CustomBundleConfigFile="$(AndroidBundleConfigurationFile)" AndroidRuntime="$(_AndroidRuntime)" ProjectRuntimeConfigFilePath="$(ProjectRuntimeConfigFilePath)" @@ -1789,13 +1680,10 @@ because xbuild doesn't support framework reference assemblies. @@ -3022,7 +2910,5 @@ because xbuild doesn't support framework reference assemblies. - diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index dd3c6b35f3d..803a2f87f2b 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -901,7 +901,7 @@ public void ActivityAliasRuns ([Values] bool isRelease, [Values (AndroidRuntime. } [Test] - public void NativeAssemblyCacheWithSatelliteAssemblies ([Values] bool enableMarshalMethods, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + public void NativeAssemblyCacheWithSatelliteAssemblies ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { const bool isRelease = true; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -912,20 +912,6 @@ public void NativeAssemblyCacheWithSatelliteAssemblies ([Values] bool enableMars Assert.Ignore ("NativeAOT doesn't support individual assemblies"); } - if (enableMarshalMethods && runtime == AndroidRuntime.CoreCLR) { - // This currently fails with the following exception: - // - // error XARMM7015: System.NotSupportedException: Writing mixed-mode assemblies is not supported - // at Mono.Cecil.ModuleWriter.Write(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleWriter.WriteModule(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleDefinition.Write(String fileName, WriterParameters parameters) - // at Mono.Cecil.AssemblyDefinition.Write(String fileName, WriterParameters parameters) - // at Xamarin.Android.Tasks.MarshalMethodsAssemblyRewriter.Rewrite(Boolean brokenExceptionTransitions) in src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs:line 165 - // at Xamarin.Android.Tasks.RewriteMarshalMethods.RewriteMethods(NativeCodeGenState state, Boolean brokenExceptionTransitionsEnabled) in src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs:line 160 - Assert.Ignore ("CoreCLR: fails because of a Mono.Cecil lack of support"); - return; - } - var path = Path.Combine ("temp", TestName); var lib = new XamarinAndroidLibraryProject { IsRelease = isRelease, @@ -949,7 +935,6 @@ public void NativeAssemblyCacheWithSatelliteAssemblies ([Values] bool enableMars var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { IsRelease = isRelease, - EnableMarshalMethods = enableMarshalMethods, }; proj.SetRuntime (runtime); proj.References.Add (new BuildItem.ProjectReference ($"..\\{lib.ProjectName}\\{lib.ProjectName}.csproj", lib.ProjectName, lib.ProjectGuid)); diff --git a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs b/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs deleted file mode 100644 index ca817105d4d..00000000000 --- a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using NUnit.Framework; -using Xamarin.ProjectTools; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using System.Text; -using System.Xml.Linq; -using System.Collections.Generic; -using Xamarin.Android.Tasks; - -namespace Xamarin.Android.Build.Tests; - -[TestFixture] -[Category ("UsesDevice")] -[Category ("MayHang")] -public class MarshalMethodsGCHangTests : DeviceTest -{ - static readonly string MarshalMethodsAppRuns_PermissionManifest = @" - - - - - -"; - - static readonly string MarshalMethodsAppRuns_MainActivity = @"using Android.Media; - -namespace marshal2; - -[Activity (Label = ""@string/app_name"", MainLauncher = true)] -public class MainActivity : Activity -{ - protected override void OnCreate (Bundle? savedInstanceState) - { - base.OnCreate (savedInstanceState); - SetContentView (Resource.Layout.Main); - } - - protected override void OnStart () - { - base.OnStart (); - - try { - var mp = new MediaPlayer (); - mp.SetDataSource (new StreamMediaDataSource (new MemoryStream (new byte[65536]))); - mp.Prepare (); - } catch (Java.IO.IOException) { - GC.Collect (); - } - Console.WriteLine (""XXX:OnStart done""); - } - - class StreamMediaDataSource (System.IO.Stream data) : MediaDataSource - { - public override long Size => data.Length; - - public override int ReadAt (long position, byte[]? buffer, int offset, int size) - { - try { - Console.WriteLine ($""XXX:START StreamMediaDataSource.ReadAt {position} {buffer} {buffer?.Length ?? 0} {offset} {size}""); - - // Allocate enough to trigger GC - for (int i = 0; i < 1000; i++) { - _ = new byte[8192]; - } - - if (data.CanSeek) { - data.Seek (position, SeekOrigin.Begin); - } - return data.Read (buffer ?? [], offset, size); - } finally { - Console.WriteLine ($""XXX:END //StreamMediaDataSource.ReadAt {position} {buffer} {buffer?.Length ?? 0} {offset} {size}""); - } - } - - public override void Close () - { - data.Dispose (); - data = System.IO.Stream.Null; - } - } -} -"; - - [Test] - [Ignore ("Marshal methods are not yet working on CoreCLR: https://github.com/dotnet/android/issues/12206")] - public void MarshalMethodsAppRuns ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) - { - const bool isRelease = true; - if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { - return; - } - - if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("Not supported with NativeAOT"); - } - - var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "marshal2")) { - IsRelease = isRelease, - EnableMarshalMethods = true, - SupportedOSPlatformVersion = "24", // Minimum 24 to be able to test on Android 16 - TrimModeRelease = TrimMode.Full, - ProjectName = "marshal2", - }; - proj.SetRuntime (runtime); - proj.SetRuntimeIdentifiers (new[] { DeviceAbi }); - proj.AndroidManifest = String.Format (MarshalMethodsAppRuns_PermissionManifest, proj.PackageName); - proj.MainActivity = MarshalMethodsAppRuns_MainActivity; - proj.SetDefaultTargetDevice (); - // TODO: AndroidEnableMarshalMethods and ReadyToRun do not work together - if (runtime == AndroidRuntime.CoreCLR) { - proj.SetProperty ("PublishReadyToRun", "false"); - } - - using var apkBuilder = CreateApkBuilder (); - Assert.True (apkBuilder.Install (proj), "Project should have installed."); - ClearAdbLogcat (); - - const string expectedLogcatOutput = "XXX:OnStart done"; - Assert.IsTrue ( - MonitorAdbLogcat ( - InstallAndRunTests.CreateLineChecker (expectedLogcatOutput), - logcatFilePath: Path.Combine (Root, apkBuilder.ProjectDirectory, "startup-logcat.log"), - timeout: 60, - onMonitoringStarted: () => StartActivityAndAssert (proj) - ), - $"Output did not contain {expectedLogcatOutput}!" - ); - } -} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs index 87e2f747157..dc89f267884 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/ScannerRunner.cs @@ -45,15 +45,7 @@ public static (List entries, Dictionary new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) - .OrderBy (e => e.JavaName, StringComparer.Ordinal) - .ThenBy (e => e.ManagedName, StringComparer.Ordinal) - .ToList (); + var entries = GetLegacyEntries (javaTypes, cache); var methodsByJavaName = new Dictionary> (); foreach (var typeDef in javaTypes) { @@ -81,6 +73,17 @@ public static (List entries, Dictionary GetLegacyEntries (List javaTypes, TypeDefinitionCache cache) => + javaTypes + .Select (type => new TypeMapEntry ( + Java.Interop.Tools.TypeNameMappings.JavaNativeTypeManager.ToJniName (type, cache), + GetManagedName (type), + type.IsInterface || type.HasGenericParameters + )) + .OrderBy (entry => entry.JavaName, StringComparer.Ordinal) + .ThenBy (entry => entry.ManagedName, StringComparer.Ordinal) + .ToList (); + public static List RunLegacyConstructors (string assemblyPath, string managedTypeName) { var cache = new TypeDefinitionCache (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs index df1d24e1ea6..40cc56952f5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests/TypeDataBuilder.cs @@ -53,15 +53,7 @@ public static (Dictionary perType, List new TypeMapEntry (e.JavaName, e.ManagedName, e.SkipInJavaToManaged)) - .OrderBy (e => e.JavaName, StringComparer.Ordinal) - .ThenBy (e => e.ManagedName, StringComparer.Ordinal) - .ToList (); + var entries = ScannerRunner.GetLegacyEntries (javaTypes, cache); var perType = new Dictionary (StringComparer.Ordinal); From 68ed8d4111f2c523cd6a662890c9797726bd608d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 08:44:15 +0200 Subject: [PATCH 04/58] Preserve library Java runtime jar selection Use trimmable Java APIs for explicit library opt-ins without importing application type map targets, and retain the packaged CoreCLR/default jars for libraries without an implementation selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index abeecad2ff6..18705bbb9c2 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1347,7 +1347,9 @@ because xbuild doesn't support framework reference assemblies. Trimmable jars replace JavaProxyObject/JavaProxyThrowable with non-native implementations. --> - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' ">$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' != 'trimmable' and '$(_AndroidRuntime)' == 'CoreCLR' ">$(MSBuildThisFileDirectory)\java_runtime_clr.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' != 'trimmable' and '$(_AndroidRuntime)' != 'CoreCLR' ">$(MSBuildThisFileDirectory)\java_runtime.jar From 3b17bb5701757eb89a41f65233671d9e08367ea2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 11:05:24 +0200 Subject: [PATCH 05/58] Preserve shared CoreCLR post-trimming behavior Restore the post-ILLink assembly pipeline, linker descriptors, and dependency dumps after removing the legacy typemap targets. Keep Debug legacy fixups disabled by default while retaining Release post-trim processing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-properties.md | 6 ++-- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 33 +++++++++++++++++++ .../Tasks/LinkerTests.cs | 6 ++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 5094917572c..ba231f57d5e 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -415,9 +415,9 @@ A boolean property that controls whether untrimmed builds modify referenced assemblies to support legacy binding and resource designer behavior. These modifications include adding missing abstract interface methods, updating legacy resource designer references, and inserting `GC.KeepAlive()` calls into -older Xamarin.Android binding assemblies. Trimmed builds using the `trimmable` -type map do not run these compatibility fixups, and setting this property to -`True` does not enable them for trimmed builds. +older Xamarin.Android binding assemblies. Trimmed builds run their post-trimming +assembly fixups separately after ILLink; this property does not control those +steps. The default value is `False` when [`$(AndroidTypeMapImplementation)`](#androidtypemapimplementation) is diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 5dd6a30bb97..dafb5b42938 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -3,6 +3,7 @@ + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider @@ -30,7 +31,39 @@ DependsOnTargets="_GenerateTrimmableTypeMap"> <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --typemap-entry-assembly "$(_TypeMapAssemblyName)" + <_TrimmerDumpDependencies Condition=" '$(LinkerDumpDependencies)' == 'true' ">true + + + + + + + + <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> + <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" + Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> + + + + + + + + + + Get_AndroidAddKeepAlivesData () // Debug configuration AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: false, setLinkModeNone: false, shouldAddKeepAlives: false, runtime); - // Debug configuration, AndroidAddKeepAlives=true - AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: true, setLinkModeNone: false, shouldAddKeepAlives: true, runtime); + // The default trimmable path does not run legacy assembly fixups. + AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: true, setLinkModeNone: false, shouldAddKeepAlives: false, runtime); // Release configuration AddTestData (isRelease: true, setAndroidAddKeepAlivesTrue: false, setLinkModeNone: false, shouldAddKeepAlives: true, runtime); // Release configuration, AndroidLinkMode=None - AddTestData (isRelease: true, setAndroidAddKeepAlivesTrue: false, setLinkModeNone: true, shouldAddKeepAlives: true, runtime); + AddTestData (isRelease: true, setAndroidAddKeepAlivesTrue: false, setLinkModeNone: true, shouldAddKeepAlives: false, runtime); } AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: true, setLinkModeNone: false, From f320eadf5c9434bed3cad7ade08399ed7879c56b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 11:17:41 +0200 Subject: [PATCH 06/58] Simplify default trimmable KeepAlives test matrix Exercise the default implementation without redundant explicit typemap settings and retain explicit legacy-fixup opt-in coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tasks/LinkerTests.cs | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index 05d77b71239..392e5f7fa42 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -518,16 +518,13 @@ static IEnumerable Get_AndroidAddKeepAlivesData () } AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: true, setLinkModeNone: false, - shouldAddKeepAlives: false, AndroidRuntime.CoreCLR, typeMapImplementation: "trimmable"); - AddTestData (isRelease: false, setAndroidAddKeepAlivesTrue: true, setLinkModeNone: false, - shouldAddKeepAlives: true, AndroidRuntime.CoreCLR, typeMapImplementation: "trimmable", + shouldAddKeepAlives: true, AndroidRuntime.CoreCLR, enableLegacyCompatibilityAssemblyFixups: true); return ret; void AddTestData (bool isRelease, bool setAndroidAddKeepAlivesTrue, bool setLinkModeNone, - bool shouldAddKeepAlives, AndroidRuntime runtime, string? typeMapImplementation = null, - bool enableLegacyCompatibilityAssemblyFixups = false) + bool shouldAddKeepAlives, AndroidRuntime runtime, bool enableLegacyCompatibilityAssemblyFixups = false) { ret.Add (new object[] { isRelease, @@ -535,7 +532,6 @@ void AddTestData (bool isRelease, bool setAndroidAddKeepAlivesTrue, bool setLink setLinkModeNone, shouldAddKeepAlives, runtime, - typeMapImplementation, enableLegacyCompatibilityAssemblyFixups, }); } @@ -544,8 +540,7 @@ void AddTestData (bool isRelease, bool setAndroidAddKeepAlivesTrue, bool setLink [Test] [TestCaseSource (nameof (Get_AndroidAddKeepAlivesData))] public void AndroidAddKeepAlives (bool isRelease, bool setAndroidAddKeepAlivesTrue, bool setLinkModeNone, - bool shouldAddKeepAlives, AndroidRuntime runtime, string? typeMapImplementation, - bool enableLegacyCompatibilityAssemblyFixups) + bool shouldAddKeepAlives, AndroidRuntime runtime, bool enableLegacyCompatibilityAssemblyFixups) { if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; @@ -555,22 +550,6 @@ public void AndroidAddKeepAlives (bool isRelease, bool setAndroidAddKeepAlivesTr return; } - if (runtime == AndroidRuntime.CoreCLR && isRelease && !setAndroidAddKeepAlivesTrue && setLinkModeNone && shouldAddKeepAlives) { - // This currently fails with the following exception: - // - // error XALNS7015: System.NotSupportedException: Writing mixed-mode assemblies is not supported - // at Mono.Cecil.ModuleWriter.Write(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleWriter.WriteModule(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleDefinition.Write(String fileName, WriterParameters parameters) - // at Mono.Cecil.AssemblyDefinition.Write(String fileName, WriterParameters parameters) - // at Xamarin.Android.Tasks.SaveChangedAssemblyStep.ProcessAssembly(AssemblyDefinition assembly, StepContext context) in src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs:line 197 - // at Xamarin.Android.Tasks.AssemblyPipeline.Run(AssemblyDefinition assembly, StepContext context) in src/Xamarin.Android.Build.Tasks/Utilities/AssemblyPipeline.cs:line 26 - // at Xamarin.Android.Tasks.AssemblyModifierPipeline.RunPipeline(AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination) in src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs:line 175 - // at Xamarin.Android.Tasks.AssemblyModifierPipeline.RunTask() in src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs:line 123 - Assert.Ignore ("CoreCLR: fails because of a Mono.Cecil lack of support"); - return; - }; - var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, OtherBuildItems = { @@ -602,8 +581,6 @@ public unsafe bool MyMethod (Android.OS.IBinder windowToken, [global::Android.Ru proj.SetRuntime (runtime); proj.SetProperty ("AllowUnsafeBlocks", "True"); - if (!typeMapImplementation.IsNullOrEmpty ()) - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); // We don't want `[TargetPlatform ("android35")]` to get set because we don't do AddKeepAlives on .NET for Android assemblies proj.SetProperty ("GenerateAssemblyInfo", "False"); From c817170f57594f7372dfddf42136caec945e19a8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 14:06:50 +0200 Subject: [PATCH 07/58] Test managed typemap updates during fast deployment Check generated managed mappings and Java sources instead of native typemap compilation while preserving APK reinstall and device behavior checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Tests/FastDevTest.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index e3f31352934..4f3b85dacc2 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -48,15 +48,13 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () }; proj.SetDefaultTargetDevice (); - // Fast deployment only syncs managed assemblies, so anything that changes the set of - // Java-callable types has to go through a new .apk: new Java stubs, a new .dex, a new - // type map inside libxamarin-app.so, and therefore a new signed package. + // A new Java-callable type changes the managed type map and Java sources. Its new + // class must be compiled into the DEX and installed with a new signed package. var typeMapTargets = new [] { + "_GenerateTrimmableTypeMap", "_GenerateJavaStubs", "_CompileJava", "_CompileToDalvik", - "_CompileNativeAssemblySources", - "_CreateApplicationSharedLibraries", "_BuildApkFastDev", "_Sign", }; @@ -69,6 +67,9 @@ public void FastDeployUpdatesTypeMapAfterAssemblyEdit () Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been installed by the initial build."); AssertActivityStarts ("MainActivity", "initial-launch.log"); + var typeMapAssembly = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "_UnnamedProject.TypeMap.dll")); + FileAssert.Exists (typeMapAssembly, "Initial build should generate the managed application type map."); + byte [] originalTypeMap = File.ReadAllBytes (typeMapAssembly); // 2. A C#-only change that adds a new Java-callable type, so the type map *must* be updated. proj.MainActivity = proj.DefaultMainActivity @@ -91,17 +92,26 @@ protected override void OnCreate (Bundle bundle) foreach (var target in typeMapTargets) { builder.Output.AssertTargetIsNotSkipped (target, occurrence: 2); } + FileAssert.Exists (typeMapAssembly, "New activity should regenerate the managed application type map."); + byte [] updatedTypeMap = File.ReadAllBytes (typeMapAssembly); + Assert.IsFalse (originalTypeMap.SequenceEqual (updatedTypeMap), "Adding a Java-callable activity should change the managed type map."); + string javaSourceDirectory = builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "java")); + Assert.AreEqual (1, Directory.GetFiles (javaSourceDirectory, "SecondActivity.java", SearchOption.AllDirectories).Length, + "Adding a Java-callable activity should generate its Java source."); Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been reinstalled after adding a new activity."); AssertActivityStarts ("SecondActivity", "new-activity-launch.log"); - // 3. A C#-only change that leaves the Java-callable types alone. The type map is unchanged, - // so the .apk is neither rebuilt nor reinstalled and only the assembly is fast deployed. + // 3. A C#-only change that leaves the Java-callable types alone keeps the managed + // type map and DEX unchanged; only the updated assembly is fast deployed. proj.MainActivity = proj.MainActivity.Replace ("//${SECOND_ACTIVITY_ONCREATE}", $"Console.WriteLine (\"{logcatMessage}\");"); proj.Touch ("MainActivity.cs"); Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Incremental install should have succeeded."); builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 3); - foreach (var target in new [] { "_CompileNativeAssemblySources", "_CreateApplicationSharedLibraries", "_BuildApkFastDev", "_Sign" }) { + builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap", occurrence: 3); + CollectionAssert.AreEqual (updatedTypeMap, File.ReadAllBytes (typeMapAssembly), + "A method-body edit should not change the managed type map."); + foreach (var target in new [] { "_CompileJava", "_CompileToDalvik", "_BuildApkFastDev", "_Sign" }) { builder.Output.AssertTargetIsSkipped (target, occurrence: 3); } Assert.IsFalse (builder.Output.IsApkInstalled, "The .apk should not be reinstalled for a C#-only change."); From 576764b7d90dd3890a5738b11814f3196aedaf01 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 14:17:18 +0200 Subject: [PATCH 08/58] Initialize CoreCLR post-trim property defaults in inner builds Run the existing properties-cache target before post-trimming so per-RID builds see the Release AndroidAddKeepAlives default when rewriting linked assemblies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index dafb5b42938..0ae0ec132e8 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -50,7 +50,7 @@ From bc8e581a7152bf857422db51b6e136c81ddc0bdc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 16:25:47 +0200 Subject: [PATCH 09/58] Gate legacy abstract-method fixups after trimming Use the existing compatibility switch to avoid resolving a trimmed AbstractMethodError constructor while retaining AppDomain diagnostics and independent post-trim steps. Cover the disabled path with a Cecil assembly test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../PostTrimmingFixAbstractMethodsStep.cs | 12 +++-- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 3 +- .../Tasks/PostTrimmingPipeline.cs | 10 ++-- .../Tasks/LinkerTests.cs | 46 +++++++++++++++++++ 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingFixAbstractMethodsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingFixAbstractMethodsStep.cs index dbf3f3853e4..565a6cb4442 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingFixAbstractMethodsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/PostTrimmingFixAbstractMethodsStep.cs @@ -10,23 +10,25 @@ namespace MonoDroid.Tuner; /// /// Post-trimming version of FixAbstractMethodsStep that delegates to the core logic -/// in . Skips framework assemblies, checks for -/// AppDomain.CreateDomain usage, and fixes missing abstract method implementations -/// on Java.Lang.Object subclasses. +/// in . Skips framework assemblies and checks for +/// AppDomain.CreateDomain usage; legacy builds also fix missing abstract methods. /// class PostTrimmingFixAbstractMethodsStep : IAssemblyModifierPipelineStep { readonly FixAbstractMethodsStep _step; readonly Action _warn; + readonly bool _enableLegacyCompatibilityAssemblyFixups; public PostTrimmingFixAbstractMethodsStep ( IMetadataResolver cache, Func getMonoAndroidAssembly, Action logMessage, - Action warn) + Action warn, + bool enableLegacyCompatibilityAssemblyFixups) { _step = new FixAbstractMethodsStep (cache, getMonoAndroidAssembly, logMessage); _warn = warn; + _enableLegacyCompatibilityAssemblyFixups = enableLegacyCompatibilityAssemblyFixups; } public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) @@ -36,7 +38,7 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) _step.CheckAppDomainUsage (assembly, _warn); - if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object")) + if (!_enableLegacyCompatibilityAssemblyFixups || !assembly.MainModule.HasTypeReference ("Java.Lang.Object")) return; context.IsAssemblyModified |= _step.FixAbstractMethods (assembly); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 0ae0ec132e8..9b966f4b7e8 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -58,7 +58,8 @@ Assemblies="@(_PostTrimmingAssembly)" AddKeepAlives="$(AndroidAddKeepAlives)" AndroidLinkResources="$(AndroidLinkResources)" - Deterministic="$(Deterministic)" /> + Deterministic="$(Deterministic)" + EnableLegacyCompatibilityAssemblyFixups="$(AndroidEnableLegacyCompatibilityAssemblyFixups)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs index fa3c3385737..30f6ac65759 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs @@ -16,7 +16,7 @@ namespace Xamarin.Android.Tasks; /// This opens each assembly once (via DirectoryAssemblyResolver with ReadWrite) and /// runs all registered steps on it, then writes modified assemblies in-place. Currently /// runs CheckForObsoletePreserveAttributeStep, StripEmbeddedLibrariesStep and -/// (optionally) AddKeepAlivesStep. +/// (optionally) AddKeepAlivesStep and legacy abstract-method fixups. /// /// Runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation, /// so that R2R images are generated from the already-modified assemblies. @@ -34,6 +34,8 @@ public class PostTrimmingPipeline : AndroidTask public bool Deterministic { get; set; } + public bool EnableLegacyCompatibilityAssemblyFixups { get; set; } = true; + public override bool RunTask () { using var resolver = new DirectoryAssemblyResolver ( @@ -66,8 +68,7 @@ public override bool RunTask () steps.Add (new RemoveResourceDesignerStep (allAssemblies, (msg) => Log.LogDebugMessage (msg))); } - // FixAbstractMethods — resolve Mono.Android once up front. If resolution fails, log - // the error and skip running the fix step entirely to avoid later unhandled exceptions. + // Resolve Mono.Android once for AppDomain warnings and optional legacy abstract-method fixups. AssemblyDefinition? monoAndroidAssembly = null; try { monoAndroidAssembly = resolver.Resolve (AssemblyNameReference.Parse ("Mono.Android")); @@ -78,7 +79,8 @@ public override bool RunTask () steps.Add (new PostTrimmingFixAbstractMethodsStep (cache, () => monoAndroidAssembly, (msg) => Log.LogDebugMessage (msg), - (msg) => Log.LogCodedWarning ("XA2000", msg))); + (msg) => Log.LogCodedWarning ("XA2000", msg), + EnableLegacyCompatibilityAssemblyFixups)); } if (AddKeepAlives) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index 392e5f7fa42..2301fe2f39e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.Json; using Java.Interop.Tools.Cecil; +using Microsoft.Build.Utilities; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Linker; @@ -55,6 +56,51 @@ public void BuildPipelineForTest (AssemblyPipeline pipeline, MSBuildLinkContext BuildPipeline (pipeline, context); } + [Test] + public void PostTrimmingSkipsLegacyAbstractFixupsButWarnsAboutAppDomain () + { + var path = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (path); + var assemblyPath = Path.Combine (path, "LegacyApp.dll"); + using (var monoAndroid = CreateFauxMonoAndroidAssembly ()) { + CreateAbstractIfaceImplementation (assemblyPath, monoAndroid); + } + + using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath, new ReaderParameters { InMemory = true })) { + var createDomain = typeof (AppDomain).GetMethod ("CreateDomain", new [] { typeof (string) }); + if (createDomain is null) + throw new InvalidOperationException ("System.AppDomain.CreateDomain(string) not found."); + + var module = assembly.MainModule; + var method = new MethodDefinition ("CreateAppDomain", MethodAttributes.Public | MethodAttributes.Static, module.TypeSystem.Void); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ldstr, "example")); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Call, module.ImportReference (createDomain))); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Pop)); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + module.GetType ("MyNamespace.MyClass").Methods.Add (method); + assembly.Write (assemblyPath); + } + + using (var assembly = AssemblyDefinition.ReadAssembly (assemblyPath)) { + var warnings = new List (); + var step = new PostTrimmingFixAbstractMethodsStep ( + new TypeDefinitionCache (), + () => throw new InvalidOperationException ("Legacy abstract-method fixup should not run."), + _ => {}, + warnings.Add, + enableLegacyCompatibilityAssemblyFixups: false); + var item = new TaskItem (assemblyPath); + var context = new StepContext (item, item); + step.ProcessAssembly (assembly, context); + + Assert.IsFalse (context.IsAssemblyModified); + Assert.IsFalse (assembly.MainModule.GetType ("MyNamespace.MyClass").Methods.Any (m => m.Name == "MyMissingMethod")); + Assert.That (warnings, Has.Count.EqualTo (1)); + StringAssert.Contains ("AppDomain.CreateDomain()", warnings [0]); + } + Directory.Delete (path, true); + } + [TestCase (false)] [TestCase (true)] public void RuntimeEventSourceFeatureSwitch (bool enabled) From 28008f481adf4f0f8a4a50bd39546b6cc648e5ff Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 11:14:17 +0200 Subject: [PATCH 10/58] Rebaseline trimmable release APKs and manifest error assertion Match three measured CI 1610334 CoreCLR APK descriptions after managed type maps replace legacy native/Java output. Preserve per-entry and package-size regression thresholds; the generated simple APK still contains its launcher and runtime provider. Assert the coded AMM0000 error and explanation without depending on manifest component order. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ManifestTest.cs | 12 ++---------- ...ReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc | 16 ++++++++-------- ...ildReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 16 ++++++++-------- ...ReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc | 18 +++++++++--------- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs index 38fcb75ff2a..8a57d79a7e6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs @@ -1226,16 +1226,8 @@ public void ExportedErrorMessage ([Values (AndroidRuntime.CoreCLR, AndroidRuntim using var b = CreateDllBuilder (); b.ThrowOnBuildFailure = false; Assert.IsFalse (b.Build (proj), "Build should have failed"); - var extension = IsWindows ? ".exe" : ""; - if (runtime == AndroidRuntime.NativeAOT) { - // The trimmable manifest generator emits the merged components in a different - // (but valid) order than the legacy path, so the offending lands on a - // different manifest line. Assert the coded AMM0000 error itself rather than the - // exact line/column, which is an implementation detail of the manifest layout. - Assert.IsTrue (b.LastBuildOutput.ContainsText ($"java{extension} error AMM0000:"), "Should receive AMM0000 error"); - } else { - Assert.IsTrue (b.LastBuildOutput.ContainsText ($"AndroidManifest.xml(12,5): java{extension} error AMM0000:"), "Should receive AMM0000 error"); - } + // The trimmable manifest generator can change the component order and error location. + Assert.IsTrue (b.LastBuildOutput.ContainsText ("error AMM0000:"), "Should receive AMM0000 error"); Assert.IsTrue (b.LastBuildOutput.ContainsText ("Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`"), "Should receive AMM0000 error"); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc index 0858d068f11..2da995c8092 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.R8.apkdesc @@ -2,22 +2,22 @@ "Comment": null, "Entries": { "AndroidManifest.xml": { - "Size": 3036 + "Size": 2964 }, "classes.dex": { - "Size": 382236 + "Size": 19668 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2529368 + "Size": 2405160 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818032 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4844000 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 188600 + "Size": 187944 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -32,7 +32,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20432 + "Size": 11664 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 6743483 + "PackageSize": 6538683 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index d2e84ee410e..bfabae9a12e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -2,22 +2,22 @@ "Comment": null, "Entries": { "AndroidManifest.xml": { - "Size": 3036 + "Size": 2964 }, "classes.dex": { - "Size": 405040 + "Size": 22252 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 2529336 + "Size": 2405168 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818032 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4844000 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 188600 + "Size": 187944 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -32,7 +32,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20432 + "Size": 11664 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 6751675 + "PackageSize": 6542779 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc index de61cb7ae8c..93e7cff7cb9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.R8.apkdesc @@ -2,10 +2,10 @@ "Comment": null, "Entries": { "AndroidManifest.xml": { - "Size": 6652 + "Size": 6768 }, "classes.dex": { - "Size": 2794592 + "Size": 2490012 }, "kotlin/annotation/annotation.kotlin_builtins": { "Size": 928 @@ -29,16 +29,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9457592 + "Size": 9836600 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843824 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 196760 + "Size": 187944 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -53,7 +53,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 119784 + "Size": 13064 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -1901,7 +1901,7 @@ "Size": 496 }, "res/layout/flyoutcontent.xml": { - "Size": 776 + "Size": 780 }, "res/layout/image_frame.xml": { "Size": 1088 @@ -2210,5 +2210,5 @@ "Size": 794696 } }, - "PackageSize": 15821708 + "PackageSize": 16108428 } \ No newline at end of file From 6f3cba092744430177ab244aa7afa340c34b6f8a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 11:23:19 +0200 Subject: [PATCH 11/58] Rebaseline single-DEX trimmable Xamarin.Forms APK CI 1610334's CoreCLR XForms release APK places the previously separate classes2.dex content in classes.dex. Match the measured APK description so apkdiff continues checking per-file and package size instead of aborting on the removed classes2.dex key. The APK still contains MainActivity, FormsViewGroup, and MonoRuntimeProvider. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...ldReleaseArm64XFormsDotNet.CoreCLR.apkdesc | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc index 49dedb83b32..a534c46c7c9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.CoreCLR.apkdesc @@ -2,13 +2,10 @@ "Comment": null, "Entries": { "AndroidManifest.xml": { - "Size": 6652 + "Size": 6768 }, "classes.dex": { - "Size": 9449044 - }, - "classes2.dex": { - "Size": 85024 + "Size": 9090932 }, "kotlin/annotation/annotation.kotlin_builtins": { "Size": 928 @@ -32,16 +29,16 @@ "Size": 2396 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 9457608 + "Size": 9836592 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2818048 + "Size": 2819768 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4843824 + "Size": 4844368 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 196760 + "Size": 187944 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72432 @@ -56,7 +53,7 @@ "Size": 171584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 119784 + "Size": 13064 }, "META-INF/androidx.activity_activity.version": { "Size": 6 @@ -1904,7 +1901,7 @@ "Size": 496 }, "res/layout/flyoutcontent.xml": { - "Size": 776 + "Size": 780 }, "res/layout/image_frame.xml": { "Size": 1088 @@ -2213,5 +2210,5 @@ "Size": 794696 } }, - "PackageSize": 18209734 + "PackageSize": 18447244 } \ No newline at end of file From 7fda946709ea8918f3be99aa6c89e59bf81c2edf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:34:34 +0200 Subject: [PATCH 12/58] Restore CoreCLR remote-process provider generation Keep generating additional Java runtime providers for trimmable CoreCLR apps after removing the legacy GenerateAdditionalProviderSources task. Register a dedicated task and track generated Java files for incremental Clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 9 +++++++ .../GenerateCoreClrRuntimeProviderSources.cs | 22 ++++++++++++++++ ...erateCoreClrRuntimeProviderSourcesTests.cs | 25 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateCoreClrRuntimeProviderSources.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCoreClrRuntimeProviderSourcesTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 30c4a2fc948..3618848a07f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -5,6 +5,7 @@ + + @@ -460,6 +462,11 @@ AdditionalProviderSources="@(_AdditionalProviderSources)" EnableSGenConcurrent="$(AndroidEnableSGenConcurrent)" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCoreClrRuntimeProviderSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCoreClrRuntimeProviderSources.cs new file mode 100644 index 00000000000..bd5d447f8b8 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCoreClrRuntimeProviderSources.cs @@ -0,0 +1,22 @@ +#nullable enable +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; + +namespace Xamarin.Android.Tasks; + +public sealed class GenerateCoreClrRuntimeProviderSources : AndroidTask +{ + public override string TaskPrefix => "GCRP"; + + [Required] + public string [] AdditionalProviderSources { get; set; } = []; + + [Required] + public string OutputDirectory { get; set; } = ""; + + public override bool RunTask () + { + GenerateAdditionalProviderSources.WriteAdditionalRuntimeProviderSources (OutputDirectory, isCoreCLR: true, AdditionalProviderSources); + return !Log.HasLoggedErrors; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCoreClrRuntimeProviderSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCoreClrRuntimeProviderSourcesTests.cs new file mode 100644 index 00000000000..0ceb33de209 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCoreClrRuntimeProviderSourcesTests.cs @@ -0,0 +1,25 @@ +using System.IO; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +public class GenerateCoreClrRuntimeProviderSourcesTests : BaseTest +{ + [Test] + public void GeneratesAdditionalProcessProvider () + { + var outputDirectory = Path.Combine (Root, "temp", TestName, "android"); + var task = new GenerateCoreClrRuntimeProviderSources { + BuildEngine = new MockBuildEngine (TestContext.Out), + AdditionalProviderSources = ["MonoRuntimeProvider_1"], + OutputDirectory = outputDirectory, + }; + + Assert.IsTrue (task.Execute (), "The additional provider should be generated."); + var provider = Path.Combine (outputDirectory, "src", "mono", "MonoRuntimeProvider_1.java"); + FileAssert.Exists (provider); + StringAssert.Contains ("class MonoRuntimeProvider_1", File.ReadAllText (provider)); + } +} From bc41f8a6ba9b11e5cbdf3c9575958a34edc83b93 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:37:23 +0200 Subject: [PATCH 13/58] Restore pre-ILLink abstract-method compatibility copies Run FixAbstractMethods on project-local assembly copies before ILLink so legacy binding interface implementations are valid and AbstractMethodError stays rooted. Swap modified RFP items while preserving metadata, avoid stale DLL/PDB timestamps and outputs, and cover old/new binding resolution without re-enabling posttrim injection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 65 ++++++ .../Tasks/PreTrimmingFixAbstractMethods.cs | 110 ++++++++++ .../Tasks/LinkerTests.cs | 193 +++++++++++++++++- 3 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixAbstractMethods.cs diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 9b966f4b7e8..ae9709150c0 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -3,12 +3,77 @@ + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider + + + + <_AndroidPreLinkAssembly Remove="@(_AndroidPreLinkAssembly)" /> + <_AndroidPreLinkAssembly Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> + + + + + + <_AndroidPreLinkModifiedAssembliesList>$(IntermediateOutputPath)prelink/modified-assemblies.txt + + + + + + <_AndroidMissingPreLinkAssembly Include="@(_AndroidExpectedPreLinkAssembly)" + Condition="!Exists('%(_AndroidExpectedPreLinkAssembly.Identity)')" /> + + + + <_AndroidExpectedPreLinkAssembly Remove="@(_AndroidExpectedPreLinkAssembly)" /> + <_AndroidMissingPreLinkAssembly Remove="@(_AndroidMissingPreLinkAssembly)" /> + + + + + + + + + + + + + + <_AndroidPreLinkSwappableAssembly Include="@(ResolvedFileToPublish)" + Condition=" '%(ResolvedFileToPublish.Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' and Exists('$(IntermediateOutputPath)prelink/%(ResolvedFileToPublish.Filename)%(ResolvedFileToPublish.Extension)') " /> + + + + + <_AndroidModifiedPreLinkAssembly Remove="@(_AndroidModifiedPreLinkAssembly)" /> + + + <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixAbstractMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixAbstractMethods.cs new file mode 100644 index 00000000000..e31263ebfc4 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixAbstractMethods.cs @@ -0,0 +1,110 @@ +#nullable enable + +using System.Collections.Generic; +using System.IO; +using Java.Interop.Tools.Cecil; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; +using MonoDroid.Tuner; + +namespace Xamarin.Android.Tasks; + +public class PreTrimmingFixAbstractMethods : AndroidTask +{ + public override string TaskPrefix => "PTA"; + + [Required] + public ITaskItem [] Assemblies { get; set; } = []; + + [Required] + public string TargetName { get; set; } = ""; + + [Required] + public string OutputDirectory { get; set; } = ""; + + public bool Deterministic { get; set; } + + [Output] + public ITaskItem [] ModifiedAssemblies { get; set; } = []; + + public override bool RunTask () + { + Directory.CreateDirectory (OutputDirectory); + + using var resolver = new DirectoryAssemblyResolver ( + this.CreateTaskLogger (), loadDebugSymbols: true); + foreach (var item in Assemblies) { + var directory = Path.GetDirectoryName (Path.GetFullPath (item.ItemSpec)); + if (directory != null && !resolver.SearchDirectories.Contains (directory)) { + resolver.SearchDirectories.Add (directory); + } + } + + var context = new MSBuildLinkContext (resolver, Log); + var step = new FixAbstractMethodsStep (); + step.Initialize (context); + var modified = new List (); + + // Resolve the app's binding assemblies before resolving references from + // libraries compiled against an older version of the same binding. + foreach (var item in Assemblies) { + if (IsPostprocessAssembly (item) && + resolver.Load (item.ItemSpec, forceLoad: true) == null) { + throw new FileNotFoundException ("Could not load prelink assembly.", item.ItemSpec); + } + } + + foreach (var item in Assemblies) { + if (Path.GetFileNameWithoutExtension (item.ItemSpec) == TargetName || + !IsPostprocessAssembly (item)) { + continue; + } + + var assembly = resolver.GetAssembly (item.ItemSpec); + var outputPath = Path.Combine (OutputDirectory, Path.GetFileName (item.ItemSpec)); + if (step.FixAbstractMethods (assembly)) { + Log.LogDebugMessage ($" Writing modified assembly: {outputPath}"); + var temporaryPath = outputPath + ".tmp.dll"; + var temporarySymbols = Path.ChangeExtension (temporaryPath, ".pdb"); + var outputSymbols = Path.ChangeExtension (outputPath, ".pdb"); + try { + assembly.Write (temporaryPath, new WriterParameters { + WriteSymbols = assembly.MainModule.HasSymbols, + DeterministicMvid = Deterministic, + }); + Files.CopyIfChanged (temporaryPath, outputPath); + if (File.Exists (temporarySymbols)) { + Files.CopyIfChanged (temporarySymbols, outputSymbols); + } else if (File.Exists (outputSymbols)) { + File.Delete (outputSymbols); + } + } finally { + if (File.Exists (temporaryPath)) { + File.Delete (temporaryPath); + } + if (File.Exists (temporarySymbols)) { + File.Delete (temporarySymbols); + } + } + modified.Add (new TaskItem (outputPath)); + } else { + if (File.Exists (outputPath)) { + File.Delete (outputPath); + } + var outputSymbols = Path.ChangeExtension (outputPath, ".pdb"); + if (File.Exists (outputSymbols)) { + File.Delete (outputSymbols); + } + } + } + + ModifiedAssemblies = modified.ToArray (); + return !Log.HasLoggedErrors; + } + + static bool IsPostprocessAssembly (ITaskItem item) => + bool.TryParse (item.GetMetadata ("PostprocessAssembly"), out var postprocess) && + postprocess && !MonoAndroidHelper.IsFrameworkAssembly (item); +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index 2301fe2f39e..8c744dd0e2c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -245,7 +245,7 @@ public void FixAbstractMethodsStep_SkipDimMembers () Directory.Delete (path, true); } - static void CreateAbstractIfaceImplementation (string assemblyPath, AssemblyDefinition android) + static void CreateAbstractIfaceImplementation (string assemblyPath, AssemblyDefinition android, bool writeSymbols = false) { using (var assm = AssemblyDefinition.CreateAssembly (new AssemblyNameDefinition ("DimTest", new Version ()), "DimTest", ModuleKind.Dll)) { var void_type = assm.MainModule.ImportReference (typeof (void)); @@ -267,8 +267,197 @@ static void CreateAbstractIfaceImplementation (string assemblyPath, AssemblyDefi impl.Interfaces.Add (new InterfaceImplementation (iface)); assm.MainModule.Types.Add (impl); - assm.Write (assemblyPath); + var parameters = new WriterParameters { WriteSymbols = writeSymbols }; + if (writeSymbols) { + parameters.SymbolWriterProvider = new PortablePdbWriterProvider (); + } + assm.Write (assemblyPath, parameters); + } + } + + [Test] + public void PreTrimmingFixAbstractMethodsWritesOnlyModifiedCopies () + { + var path = Path.Combine (Root, "temp", TestName); + var outputDirectory = Path.Combine (path, "prelink"); + Directory.CreateDirectory (path); + try { + var androidPath = Path.Combine (path, "Mono.Android.dll"); + var libraryPath = Path.Combine (path, "MyAssembly.dll"); + using (var android = CreateFauxMonoAndroidAssembly ()) { + android.Write (androidPath); + CreateAbstractIfaceImplementation (libraryPath, android); + } + + var library = new Microsoft.Build.Utilities.TaskItem (libraryPath); + library.SetMetadata ("PostprocessAssembly", "True"); + var task = new PreTrimmingFixAbstractMethods { + Assemblies = [new Microsoft.Build.Utilities.TaskItem (typeof (object).Assembly.Location), + new Microsoft.Build.Utilities.TaskItem (androidPath), library], + TargetName = "App", + OutputDirectory = outputDirectory, + Deterministic = true, + BuildEngine = new MockBuildEngine (TestContext.Out), + }; + + Assert.IsTrue (task.Execute ()); + var copy = Path.Combine (outputDirectory, "MyAssembly.dll"); + FileAssert.Exists (copy); + Assert.AreEqual (copy, task.ModifiedAssemblies.Single ().ItemSpec); + Assert.IsFalse (File.Exists (Path.Combine (outputDirectory, "Mono.Android.dll"))); + + using (var original = AssemblyDefinition.ReadAssembly (libraryPath)) + using (var modified = AssemblyDefinition.ReadAssembly (copy)) { + Assert.IsFalse (original.MainModule.GetType ("MyNamespace.MyClass").Methods.Any (method => method.Name == "MyAbstractMethod")); + var newMethod = modified.MainModule.GetType ("MyNamespace.MyClass").Methods.Single (method => method.Name == "MyAbstractMethod"); + Assert.IsTrue (newMethod.Body.Instructions.Any (instruction => + instruction.Operand is MethodReference constructor && + constructor.DeclaringType.FullName == "Java.Lang.AbstractMethodError")); + } + + File.SetLastWriteTimeUtc (copy, DateTime.UtcNow.AddMinutes (-2)); + var writeTime = File.GetLastWriteTimeUtc (copy); + Assert.IsTrue (task.Execute ()); + Assert.AreEqual (writeTime, File.GetLastWriteTimeUtc (copy), + "an unchanged abstract-method fixup must not invalidate FastDeploy inputs"); + + var updatedLibraryPath = Path.Combine (path, "UpdatedMyAssembly.dll"); + using (var assembly = AssemblyDefinition.ReadAssembly (libraryPath)) { + var implementation = assembly.MainModule.GetType ("MyNamespace.MyClass"); + var method = new MethodDefinition ("MyAbstractMethod", MethodAttributes.Public | MethodAttributes.Virtual, + assembly.MainModule.TypeSystem.Void); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + implementation.Methods.Add (method); + assembly.Write (updatedLibraryPath); + } + File.Copy (updatedLibraryPath, libraryPath, overwrite: true); + Assert.IsTrue (task.Execute ()); + Assert.IsEmpty (task.ModifiedAssemblies); + Assert.IsFalse (File.Exists (copy), "a stale prelink copy must not replace the updated library"); + } finally { + Directory.Delete (path, recursive: true); + } + } + + [Test] + public void PreTrimmingFixAbstractMethodsPreservesUnchangedSymbols () + { + var path = Path.Combine (Root, "temp", TestName); + var outputDirectory = Path.Combine (path, "prelink"); + Directory.CreateDirectory (path); + try { + var androidPath = Path.Combine (path, "Mono.Android.dll"); + var libraryPath = Path.Combine (path, "MyAssembly.dll"); + using (var android = CreateFauxMonoAndroidAssembly ()) { + android.Write (androidPath); + CreateAbstractIfaceImplementation (libraryPath, android, writeSymbols: true); + } + + var library = new Microsoft.Build.Utilities.TaskItem (libraryPath); + library.SetMetadata ("PostprocessAssembly", "true"); + var task = new PreTrimmingFixAbstractMethods { + Assemblies = [new Microsoft.Build.Utilities.TaskItem (typeof (object).Assembly.Location), + new Microsoft.Build.Utilities.TaskItem (androidPath), library], + TargetName = "App", + OutputDirectory = outputDirectory, + Deterministic = true, + BuildEngine = new MockBuildEngine (TestContext.Out), + }; + + Assert.IsTrue (task.Execute ()); + var outputDll = Path.Combine (outputDirectory, "MyAssembly.dll"); + var outputPdb = Path.ChangeExtension (outputDll, ".pdb"); + FileAssert.Exists (outputDll); + FileAssert.Exists (outputPdb); + File.SetLastWriteTimeUtc (outputDll, DateTime.UtcNow.AddMinutes (-2)); + File.SetLastWriteTimeUtc (outputPdb, DateTime.UtcNow.AddMinutes (-2)); + var dllWriteTime = File.GetLastWriteTimeUtc (outputDll); + var pdbWriteTime = File.GetLastWriteTimeUtc (outputPdb); + + Assert.IsTrue (task.Execute ()); + Assert.AreEqual (dllWriteTime, File.GetLastWriteTimeUtc (outputDll)); + Assert.AreEqual (pdbWriteTime, File.GetLastWriteTimeUtc (outputPdb)); + Assert.IsFalse (File.Exists (outputDll + ".tmp.dll")); + Assert.IsFalse (File.Exists (Path.ChangeExtension (outputDll + ".tmp.dll", ".pdb"))); + } finally { + Directory.Delete (path, recursive: true); + } + } + + [Test] + public void PreTrimmingFixAbstractMethodsUsesSelectedBindingVersion () + { + var path = Path.Combine (Root, "temp", TestName); + var oldDirectory = Path.Combine (path, "old"); + var newDirectory = Path.Combine (path, "new"); + Directory.CreateDirectory (oldDirectory); + Directory.CreateDirectory (newDirectory); + try { + var androidPath = Path.Combine (path, "Mono.Android.dll"); + var oldBindingPath = Path.Combine (oldDirectory, "TestBinding.dll"); + var newBindingPath = Path.Combine (newDirectory, "TestBinding.dll"); + var libraryPath = Path.Combine (oldDirectory, "LegacyLibrary.dll"); + var nativePath = Path.Combine (oldDirectory, "NativeDependency.dll"); + File.WriteAllBytes (nativePath, [0, 1, 2, 3]); + using (var android = CreateFauxMonoAndroidAssembly ()) { + android.Write (androidPath); + CreateBindingInterface (oldBindingPath, includeNewMethod: false); + CreateBindingInterface (newBindingPath, includeNewMethod: true); + using var oldBinding = AssemblyDefinition.ReadAssembly (oldBindingPath); + using var library = AssemblyDefinition.CreateAssembly ( + new AssemblyNameDefinition ("LegacyLibrary", new Version (1, 0)), "LegacyLibrary", ModuleKind.Dll); + var implementation = new TypeDefinition ("Legacy", "Cursor", TypeAttributes.Public, + library.MainModule.ImportReference (android.MainModule.GetType ("Java.Lang.Object"))); + implementation.Interfaces.Add (new InterfaceImplementation ( + library.MainModule.ImportReference (oldBinding.MainModule.GetType ("Test.Bindings.ICursor")))); + var method = new MethodDefinition ("Method", MethodAttributes.Public | MethodAttributes.Virtual, + library.MainModule.TypeSystem.Void); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + implementation.Methods.Add (method); + library.MainModule.Types.Add (implementation); + library.Write (libraryPath); + } + + var oldLibrary = new Microsoft.Build.Utilities.TaskItem (libraryPath); + oldLibrary.SetMetadata ("PostprocessAssembly", "true"); + var newBinding = new Microsoft.Build.Utilities.TaskItem (newBindingPath); + newBinding.SetMetadata ("PostprocessAssembly", "true"); + var task = new PreTrimmingFixAbstractMethods { + Assemblies = [new Microsoft.Build.Utilities.TaskItem (nativePath), oldLibrary, newBinding, + new Microsoft.Build.Utilities.TaskItem (androidPath), + new Microsoft.Build.Utilities.TaskItem (typeof (object).Assembly.Location)], + TargetName = "App", + OutputDirectory = Path.Combine (path, "prelink"), + BuildEngine = new MockBuildEngine (TestContext.Out), + }; + + Assert.IsTrue (task.Execute ()); + var copy = Path.Combine (task.OutputDirectory, "LegacyLibrary.dll"); + FileAssert.Exists (copy); + using var modified = AssemblyDefinition.ReadAssembly (copy); + Assert.IsTrue (modified.MainModule.GetType ("Legacy.Cursor").Methods.Any (member => member.Name == "NewMethod"), + "the current binding interface, not the nearby older DLL, must determine missing methods"); + } finally { + Directory.Delete (path, recursive: true); + } + } + + static void CreateBindingInterface (string path, bool includeNewMethod) + { + using var assembly = AssemblyDefinition.CreateAssembly ( + new AssemblyNameDefinition ("TestBinding", new Version (1, 0)), "TestBinding", ModuleKind.Dll); + var cursor = new TypeDefinition ("Test.Bindings", "ICursor", + TypeAttributes.Interface | TypeAttributes.Abstract | TypeAttributes.Public); + cursor.Methods.Add (new MethodDefinition ("Method", + MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, + assembly.MainModule.TypeSystem.Void)); + if (includeNewMethod) { + cursor.Methods.Add (new MethodDefinition ("NewMethod", + MethodAttributes.Public | MethodAttributes.Abstract | MethodAttributes.Virtual, + assembly.MainModule.TypeSystem.Void)); } + assembly.MainModule.Types.Add (cursor); + assembly.Write (path); } [Test] From efd19a319862f7039fd86a5aaaca1383cb5fec1e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:38:14 +0200 Subject: [PATCH 14/58] Select the effective post-trim RID and configure Proguard Device builds can change RuntimeIdentifier after evaluation; use the selected RID when collecting post-trim Java inputs so x64 FastDeploy retains launcher JCWs. Set the Proguard output before ILLink and track it for Clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ....Android.Sdk.TypeMap.Trimmable.CoreCLR.targets | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index ae9709150c0..c948f1e76e7 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -78,6 +78,14 @@ <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink + + + <_ProguardProjectConfiguration Condition=" '$(_ProguardProjectConfiguration)' == '' ">$(IntermediateOutputPath)proguard\proguard_project_references.cfg + + + + + + + + + <_PostTrimTypeMapFirstRuntimeIdentifier Condition=" '$(RuntimeIdentifier)' != '' ">$(RuntimeIdentifier) + <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> + <_AndroidPreTrimTypeMapCandidate + Condition=" '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">true <_ResolvedAssemblies Include="@(_TrimmableTypeMapResolvedAssemblies)" /> <_ShrunkAssemblies Include="@(_TrimmableTypeMapResolvedAssemblies)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/FastDeployTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/FastDeployTypeMapTests.cs new file mode 100644 index 00000000000..f9de1b13da3 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/FastDeployTypeMapTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + public class FastDeployTypeMapTests : BaseTest + { + [TestCase (true, true, true, true, 2)] + [TestCase (true, true, true, false, 3)] + [TestCase (true, true, false, true, 3)] + [TestCase (true, false, true, true, 3)] + [TestCase (false, true, true, true, 1)] + public void PreTrimTypeMapYieldsOnlyToMatchingFinalCopy ( + bool includeFinal, bool finalExists, bool trimmed, bool sameAbi, int expectedCount) + { + var path = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (path); + try { + var preTrimPath = Path.Combine (path, "pretrim", "_Microsoft.Android.TypeMaps.dll"); + var finalPath = Path.Combine (path, "R2R", "_Microsoft.Android.TypeMaps.dll"); + Directory.CreateDirectory (Path.GetDirectoryName (preTrimPath)); + Directory.CreateDirectory (Path.GetDirectoryName (finalPath)); + File.WriteAllBytes (preTrimPath, [1]); + if (finalExists) { + File.WriteAllBytes (finalPath, [2]); + } + + var preTrim = new TaskItem (preTrimPath); + preTrim.SetMetadata ("TargetPath", "x86_64/_Microsoft.Android.TypeMaps.dll"); + if (trimmed) { + preTrim.SetMetadata ("_AndroidPreTrimTypeMapCandidate", "true"); + } + var final = new TaskItem (finalPath); + final.SetMetadata ("TargetPath", sameAbi + ? "x86_64\\_Microsoft.Android.TypeMaps.dll" + : "arm64-v8a/_Microsoft.Android.TypeMaps.dll"); + + var files = new List { preTrim }; + if (includeFinal) { + files.Add (final); + files.Add (new TaskItem (final)); + } + var selected = new FastDeploy ().FilterPreTrimTypeMapFiles (files.ToArray ()); + Assert.AreEqual (expectedCount, selected.Length); + Assert.AreEqual (!includeFinal || !finalExists || !trimmed || !sameAbi, + selected.Any (item => item.ItemSpec == preTrimPath)); + } finally { + Directory.Delete (path, recursive: true); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs index 59231cee332..6886977af32 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs @@ -41,4 +41,35 @@ public void HaveAssemblyStoreIsEmittedForCoreCLR (bool haveAssemblyStore) var config = (EnvironmentHelper.ApplicationConfig)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR); Assert.AreEqual (haveAssemblyStore, config.have_assembly_store); } + + [Test] + public void TypeMapAssemblyIsCountedOnceAcrossResolvedAssemblyLists () + { + string outputRoot = Path.Combine (Root, "temp", TestName); + string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll"); + FileAssert.Exists (monoAndroidPath); + string typeMapPath = Path.Combine (outputRoot, "typemap", "_Test.TypeMap.dll"); + var typeMap = new TaskItem (typeMapPath); + typeMap.SetMetadata ("Abi", "arm64-v8a"); + typeMap.SetMetadata ("DestinationSubDirectory", "arm64-v8a/"); + typeMap.SetMetadata ("RelativePath", "_Test.TypeMap.dll"); + + var task = new GenerateNativeApplicationConfigSources { + BuildEngine = new MockBuildEngine (TestContext.Out), + ResolvedAssemblies = [new TaskItem (monoAndroidPath), typeMap], + AdditionalResolvedAssemblies = [new TaskItem (typeMapPath)], + EnvironmentOutputDirectory = Path.Combine (outputRoot, "android"), + SupportedAbis = ["arm64-v8a"], + AndroidPackageName = "com.microsoft.android.typemapcounttest", + EnablePreloadAssembliesDefault = false, + AndroidRuntime = "CoreCLR", + UseAssemblyStore = true, + }; + + Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed."); + var environmentFiles = EnvironmentHelper.GatherEnvironmentFiles ( + outputRoot, "arm64-v8a", required: true, runtime: AndroidRuntime.CoreCLR); + var config = (EnvironmentHelper.ApplicationConfig)EnvironmentHelper.ReadApplicationConfig (environmentFiles, AndroidRuntime.CoreCLR); + Assert.AreEqual (2u, config.number_of_assemblies_in_apk, "The type map must not be counted again as a satellite assembly."); + } } From 3885e7b8513ee707b83b205616ba37c374676762 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:40:45 +0200 Subject: [PATCH 16/58] Categorize Android libraries before Java compilation Nested DeployToDevice builds must convert AndroidLibrary jars into program inputs before Java library selection and D8, including Kotlin binding jars. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets index 04b17fe3b6b..f6de54356a0 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Javac.targets @@ -52,7 +52,7 @@ It is shared between "legacy" binding projects and .NET 7+ projects. + DependsOnTargets="_CategorizeAndroidLibraries;_CollectJavaSourceForBinding;_CompileBindingJava"> Date: Thu, 24 Sep 2026 15:41:44 +0200 Subject: [PATCH 17/58] Preserve deserialization callback in trimmed CoreCLR apps Root Java.Lang.Object.SetHandleOnDeserialized, which DataContractJsonSerializer invokes via reflection and whose removal leaves linked Release peers without a handle. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml index 8c62546764a..eb3df072f85 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml @@ -5,5 +5,9 @@ + + + + From 85dd2393f9b5e10be75fd3e125a1d29cc3f63763 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 16:02:08 +0200 Subject: [PATCH 18/58] Remove obsolete LLVM typemap-positive build test cases Keep the CoreCLR and NativeAOT trimmable/default assertions while dropping cases that explicitly select the rejected llvm-ir typemap. Remove the legacy-only marshal postlink cache test, retain FastTiming dump coverage under the default, and leave XA4265 rejection tests intact. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/BuildTest.cs | 73 ---- .../BuildWithLibraryTests.cs | 32 +- .../TrimmableTypeMapBuildTests.cs | 401 +++++------------- .../Tests/FastTimingTests.cs | 27 +- .../Tests/InstallAndRunTests.cs | 29 +- .../Tests/WebViewJavascriptInterfaceTests.cs | 1 - 6 files changed, 122 insertions(+), 441 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 1f4891243d4..e9761a27251 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2256,79 +2256,6 @@ public void CheckLintResourceFileReferencesAreFixed ([Values (AndroidRuntime.Cor } } - [TestCase (AndroidRuntime.CoreCLR)] - public void BuildDoesNotModifyNuGetPackageCache (AndroidRuntime runtime) - { - var proj = new XamarinAndroidApplicationProject { - IsRelease = true, - GlobalPackagesFolder = Path.Combine (Root, TestName, "packages"), - Imports = { - new Import (() => "EnableMarshalMethodsForPostLink.targets") { - TextContent = () => -""" - - - - - <_AndroidUseMarshalMethods>true - - - -""" - }, - }, - PackageReferences = { - new Package { Id = "Humanizer.Core", Version = "2.14.1" }, - new Package { Id = "Humanizer.Core.es", Version = "2.14.1" }, - }, - }; - proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); - proj.SetProperty (KnownProperties.PublishTrimmed, true.ToString ()); - proj.MainActivity = proj.DefaultMainActivity - .Replace ("//${USINGS}", "using Humanizer;") - .Replace ("//${AFTER_ONCREATE}", "System.Console.WriteLine (System.DateTime.UtcNow.Humanize ());"); - - using var builder = CreateApkBuilder (); - var buildParameters = new [] { $"RestorePackagesPath={proj.GlobalPackagesFolder}" }; - Assert.IsTrue (builder.Restore (proj, parameters: buildParameters), "Package restore should have succeeded."); - Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, parameters: buildParameters), - "Initial build should have succeeded."); - - var satelliteAssemblies = Directory.GetFiles (proj.GlobalPackagesFolder, "*.resources.dll", SearchOption.AllDirectories); - Assert.IsNotEmpty (satelliteAssemblies, "The NuGet package should contain satellite assemblies."); - - var originalWriteTimes = new Dictionary (); - foreach (string assembly in satelliteAssemblies) { - File.SetLastWriteTimeUtc (assembly, new DateTime (2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); - originalWriteTimes.Add (assembly, File.GetLastWriteTimeUtc (assembly)); - } - - var postLinkStamp = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "stamp", "_AdditionalPostLinkerSteps.stamp"); - FileAssert.Exists (postLinkStamp); - File.Delete (postLinkStamp); - - // A package cache is immutable: deny write sharing and verify timestamps remain unchanged. - var packageLocks = satelliteAssemblies - .Select (assembly => File.Open (assembly, FileMode.Open, FileAccess.Read, FileShare.Read)) - .ToList (); - try { - var postLinkParameters = buildParameters.Append ("TestEnableMarshalMethodsForPostLink=true").ToArray (); - Assert.IsTrue (builder.RunTarget (proj, "_PrepareAssemblies", doNotCleanupOnUpdate: true, saveProject: false, parameters: postLinkParameters), - "Preparing assemblies should have succeeded."); - FileAssert.Exists (postLinkStamp); - foreach (string assembly in satelliteAssemblies) { - Assert.AreEqual (originalWriteTimes [assembly], File.GetLastWriteTimeUtc (assembly), $"Build should not modify '{assembly}'."); - } - } finally { - foreach (var packageLock in packageLocks) { - packageLock.Dispose (); - } - } - } - [Test] [TestCase (true, AndroidRuntime.CoreCLR)] [TestCase (false, AndroidRuntime.CoreCLR)] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index d2691211cf1..b50e01701b0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -618,10 +618,9 @@ public void AndroidLibraryProjectsZipWithOddPaths ([Values (AndroidRuntime.CoreC } } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] - [TestCase ("trimmable", AndroidRuntime.CoreCLR)] - [TestCase ("trimmable", AndroidRuntime.NativeAOT)] - public void DuplicateJCWNames (string typemapImplementation, AndroidRuntime runtime) + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void DuplicateJCWNames (AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -679,7 +678,6 @@ public SecondPeer (string value) }, }; app.SetRuntime (runtime); - app.SetProperty ("AndroidTypeMapImplementation", typemapImplementation); var projectPath = Path.Combine ("temp", $"{TestName}_{Guid.NewGuid ():N}"); using var lib1b = CreateDllBuilder (Path.Combine (projectPath, library1.ProjectName), cleanupAfterSuccessfulBuild: false); using var lib2b = CreateDllBuilder (Path.Combine (projectPath, library2.ProjectName), cleanupAfterSuccessfulBuild: false); @@ -690,8 +688,8 @@ public SecondPeer (string value) using var appb = CreateApkBuilder (Path.Combine (projectPath, app.ProjectName), cleanupAfterSuccessfulBuild: false); appb.ThrowOnBuildFailure = false; Assert.IsFalse (appb.Build (app), "Build of App1 should have failed"); -var errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")).ToList (); -Assert.IsNotEmpty (errors, "Error should be XA4215"); + var errors = appb.LastBuildOutput.Where (x => x.Contains ("error XA4215")).ToList (); + Assert.IsNotEmpty (errors, "Error should be XA4215"); StringAssertEx.Contains ("examplelib.DuplicatePeer", errors, "Error should mention the conflicting Java type name"); StringAssertEx.Contains ("Library1.FirstPeer", errors, "Error should mention the first conflicting managed type"); StringAssertEx.Contains ("Library2.SecondPeer", errors, "Error should mention the second conflicting managed type"); @@ -699,19 +697,13 @@ public SecondPeer (string value) "Distinct managed type names should not produce XA4214."); var acwMapFile = appb.Output.GetIntermediaryPath ("acw-map.txt"); - if (typemapImplementation == "llvm-ir") { - var javaFile = appb.Output.GetIntermediaryPath (Path.Combine ("android", "src", "examplelib", "DuplicatePeer.java")); - FileAssert.Exists (javaFile, "llvm-ir generates JCW sources before XA4215 is detected."); - FileAssert.DoesNotExist (acwMapFile, "llvm-ir should not write an ambiguous acw-map."); - } else { - var typemapDirectory = appb.Output.GetIntermediaryPath ("typemap"); - var javaFile = Path.Combine (typemapDirectory, "java", "examplelib", "DuplicatePeer.java"); - FileAssert.DoesNotExist (javaFile, "A failed trimmable typemap build should not write an ambiguous JCW source."); - FileAssert.DoesNotExist (acwMapFile, "A failed trimmable typemap build should not write an ambiguous acw-map."); - if (Directory.Exists (typemapDirectory)) { - Assert.IsEmpty (Directory.GetFiles (typemapDirectory, "*.dll"), - "A failed trimmable typemap build should not write typemap assemblies."); - } + var typemapDirectory = appb.Output.GetIntermediaryPath ("typemap"); + var javaFile = Path.Combine (typemapDirectory, "java", "examplelib", "DuplicatePeer.java"); + FileAssert.DoesNotExist (javaFile, "A failed trimmable typemap build should not write an ambiguous JCW source."); + FileAssert.DoesNotExist (acwMapFile, "A failed trimmable typemap build should not write an ambiguous acw-map."); + if (Directory.Exists (typemapDirectory)) { + Assert.IsEmpty (Directory.GetFiles (typemapDirectory, "*.dll"), + "A failed trimmable typemap build should not write typemap assemblies."); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 50b6febf26a..d610504fdbd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -38,14 +38,9 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value AssertTrimmableTypeMapOutputs (intermediateDir); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "APT2008", false)] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4258", true)] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4258", true)] - public void Build_JavaTypeOnlyIdentifierStarts_MatchManifestLimitation ( - string typeMapImplementation, - AndroidRuntime runtime, - string expectedCode, - bool expectNoOutputs) + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void Build_JavaTypeOnlyIdentifierStarts_MatchManifestLimitation (AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -56,7 +51,6 @@ public void Build_JavaTypeOnlyIdentifierStarts_MatchManifestLimitation ( IsRelease = isRelease, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); proj.Sources.Add (new BuildItem.Source ("ManifestIdentifierStarts.cs") { TextContent = () => """ using Android.App; @@ -91,40 +85,20 @@ public SupplementaryComponent () { } builder.ThrowOnBuildFailure = false; Assert.IsFalse ( builder.Build (proj), - $"{runtime}/{typeMapImplementation} should reject Java-only starts in manifest component names."); - StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); - - if (expectNoOutputs) { - StringAssertEx.Contains ("\u00a2Peer", builder.LastBuildOutput); - StringAssertEx.Contains ("\u203fPeer", builder.LastBuildOutput); - StringAssertEx.Contains ("\U00010428Peer", builder.LastBuildOutput); - AssertNoUnicodeOutputs (builder, "\u00a2Peer"); - AssertNoUnicodeOutputs (builder, "\u203fPeer"); - AssertNoUnicodeOutputs (builder, "\U00010428Peer"); - return; - } - - foreach (var javaName in new [] { - "com/example/\u00a2Peer", - "com/example/\u203fPeer", - "com/example/\U00010428Peer", - }) { - var relativePath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); - FileAssert.Exists ( - builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativePath)), - $"llvm-ir should generate '{javaName}.java' before AAPT rejects the manifest name."); - } + $"{runtime} should reject Java-only starts in manifest component names."); + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("\u00a2Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u203fPeer", builder.LastBuildOutput); + StringAssertEx.Contains ("\U00010428Peer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\u00a2Peer"); + AssertNoUnicodeOutputs (builder, "\u203fPeer"); + AssertNoUnicodeOutputs (builder, "\U00010428Peer"); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true)] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, false)] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, false)] - public void Build_SupplementaryJavaIdentifier_MatchesRuntimeLimitation ( - string typeMapImplementation, - AndroidRuntime runtime, - bool shouldSucceed) + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void Build_SupplementaryJavaIdentifier_MatchesRuntimeLimitation (AndroidRuntime runtime) { - const string javaName = "com/example/\U00010428Peer\U00010400"; bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; @@ -134,7 +108,6 @@ public void Build_SupplementaryJavaIdentifier_MatchesRuntimeLimitation ( IsRelease = isRelease, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); proj.Sources.Add (new BuildItem.Source ("SupplementaryJavaIdentifier.cs") { TextContent = () => """ using Android.Runtime; @@ -154,60 +127,24 @@ public SupplementaryJavaIdentifier () { } using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.AreEqual ( - shouldSucceed, - builder.Build (proj), - $"{runtime}/{typeMapImplementation} should match the supplementary runtime limitation."); - if (!shouldSucceed) { - StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); - StringAssertEx.Contains ("\U00010428Peer\U00010400", builder.LastBuildOutput); - AssertNoUnicodeOutputs (builder, "\U00010428Peer\U00010400"); - return; - } - - var relativeJavaPath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); - var javaPath = new [] { - builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativeJavaPath)), - builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "java", relativeJavaPath)), - builder.Output.GetIntermediaryPath (Path.Combine ("typemap", "linked-java", relativeJavaPath)), - }.FirstOrDefault (File.Exists); - Assert.IsNotNull (javaPath, $"{runtime}/{typeMapImplementation} should preserve the supplementary Java source path."); - StringAssert.Contains ("public class \U00010428Peer\U00010400", File.ReadAllText (javaPath)); - - var classFile = builder.Output.GetIntermediaryPath ( - Path.Combine ("android", "bin", "classes", relativeJavaPath.Replace (".java", ".class"))); - FileAssert.Exists (classFile, "javac should accept the supplementary Java identifier."); - - var acwMap = builder.Output.GetIntermediaryPath ("acw-map.txt"); - StringAssert.Contains ( - "UnnamedProject.SupplementaryJavaIdentifier;com.example.\U00010428Peer\U00010400", - File.ReadAllText (acwMap)); - - var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); - Assert.IsTrue ( - DexUtils.ContainsClass ($"L{javaName};", dexFile, AndroidSdkPath), - "llvm-ir should preserve the exact supplementary descriptor even though Android cannot load it."); + Assert.IsFalse (builder.Build (proj), $"{runtime} should reject the unsupported supplementary Java identifier."); + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("\U00010428Peer\U00010400", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\U00010428Peer\U00010400"); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "JAVAC0000")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4258")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4258")] - public void Build_InvalidOrUnsupportedUnicodeIdentifiers_ReportDiagnosticWithoutTrimmableOutputs ( - string typeMapImplementation, - AndroidRuntime runtime, - string expectedCode) + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void Build_InvalidOrUnsupportedUnicodeIdentifiers_ReportDiagnosticWithoutTrimmableOutputs (AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - bool isTrimmable = typeMapImplementation == "trimmable"; - var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); string projectSource = """ using Android.Runtime; @@ -224,35 +161,31 @@ public class InvalidCombiningStart : Java.Lang.Object { public InvalidCombiningStart () { } } - """; - if (isTrimmable) { - projectSource += """ - [Register ("com/e\u0301xample/Cafe\u0301")] - public class UnsupportedCombiningMark : Java.Lang.Object - { - public UnsupportedCombiningMark () { } - } + [Register ("com/e\u0301xample/Cafe\u0301")] + public class UnsupportedCombiningMark : Java.Lang.Object + { + public UnsupportedCombiningMark () { } + } - [Register ("com/example/A\u0cf3")] - public class Unicode15CombiningMark : Java.Lang.Object - { - public Unicode15CombiningMark () { } - } + [Register ("com/example/A\u0cf3")] + public class Unicode15CombiningMark : Java.Lang.Object + { + public Unicode15CombiningMark () { } + } - [Register ("com/example/\u1c89Peer")] - public class NewerUnicodeLetter : Java.Lang.Object - { - public NewerUnicodeLetter () { } - } + [Register ("com/example/\u1c89Peer")] + public class NewerUnicodeLetter : Java.Lang.Object + { + public NewerUnicodeLetter () { } + } - [Register ("com/example/\u212bPeer")] - public class NonNfcIdentifier : Java.Lang.Object - { - public NonNfcIdentifier () { } - } - """; - } + [Register ("com/example/\u212bPeer")] + public class NonNfcIdentifier : Java.Lang.Object + { + public NonNfcIdentifier () { } + } + """; proj.Sources.Add (new BuildItem.Source ("InvalidUnicodeIdentifiers.cs") { TextContent = () => projectSource, }); @@ -261,45 +194,25 @@ public NonNfcIdentifier () { } builder.ThrowOnBuildFailure = false; Assert.IsFalse ( builder.Build (proj), - $"{runtime}/{typeMapImplementation} should reject invalid or unsupported Java identifiers."); - StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); - - if (isTrimmable) { - StringAssertEx.Contains ("1Peer", builder.LastBuildOutput); - StringAssertEx.Contains ("\u0301Peer", builder.LastBuildOutput); - StringAssertEx.Contains ("e\u0301xample", builder.LastBuildOutput); - StringAssertEx.Contains ("A\u0cf3", builder.LastBuildOutput); - StringAssertEx.Contains ("\u1c89Peer", builder.LastBuildOutput); - StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); - AssertNoUnicodeOutputs (builder, "1Peer"); - AssertNoUnicodeOutputs (builder, "\u0301Peer"); - AssertNoUnicodeOutputs (builder, "Cafe\u0301"); - AssertNoUnicodeOutputs (builder, "A\u0cf3"); - AssertNoUnicodeOutputs (builder, "\u1c89Peer"); - AssertNoUnicodeOutputs (builder, "\u212bPeer"); - return; - } - - foreach (var javaName in new [] { - "com/example/1Peer", - "com/example/\u0301Peer", - }) { - var relativePath = (javaName + ".java").Replace ('/', Path.DirectorySeparatorChar); - var javaPath = builder.Output.GetIntermediaryPath (Path.Combine ("android", "src", relativePath)); - FileAssert.Exists (javaPath, $"llvm-ir should reach javac with '{javaName}'."); - var source = File.ReadAllText (javaPath); - StringAssert.Contains ($"public class {javaName.Substring (javaName.LastIndexOf ('/') + 1)}", source); - } + $"{runtime} should reject invalid or unsupported Java identifiers."); + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("1Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u0301Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("e\u0301xample", builder.LastBuildOutput); + StringAssertEx.Contains ("A\u0cf3", builder.LastBuildOutput); + StringAssertEx.Contains ("\u1c89Peer", builder.LastBuildOutput); + StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "1Peer"); + AssertNoUnicodeOutputs (builder, "\u0301Peer"); + AssertNoUnicodeOutputs (builder, "Cafe\u0301"); + AssertNoUnicodeOutputs (builder, "A\u0cf3"); + AssertNoUnicodeOutputs (builder, "\u1c89Peer"); + AssertNoUnicodeOutputs (builder, "\u212bPeer"); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true, false)] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, false, true)] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, false, true)] - public void Build_CanonicallyEquivalentJavaNames_DoNotOverwriteOutputs ( - string typeMapImplementation, - AndroidRuntime runtime, - bool shouldSucceed, - bool expectNoOutputs) + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void Build_CanonicallyEquivalentJavaNames_DoNotOverwriteOutputs (AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -310,7 +223,6 @@ public void Build_CanonicallyEquivalentJavaNames_DoNotOverwriteOutputs ( IsRelease = isRelease, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); proj.Sources.Add (new BuildItem.Source ("CanonicalJavaIdentifiers.cs") { TextContent = () => """ using Android.Runtime; @@ -333,83 +245,24 @@ public CanonicalEquivalentIdentifier () { } using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.AreEqual ( - shouldSucceed, - builder.Build (proj), - $"{runtime}/{typeMapImplementation} should match canonical-equivalent Java output behavior."); - - if (expectNoOutputs) { - StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); - StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); - AssertNoUnicodeOutputs (builder, "\u00c5Peer"); - AssertNoUnicodeOutputs (builder, "\u212bPeer"); - return; - } - - var javaDirectory = builder.Output.GetIntermediaryPath ( - Path.Combine ("android", "src", "com", "example")); - if (!CanonicalEquivalentPathsAlias (javaDirectory)) { - return; - } - var javaFile = AssertSingleFile (javaDirectory, "*Peer.java"); - StringAssert.Contains ("public class \u212bPeer", File.ReadAllText (javaFile)); - - var classDirectory = builder.Output.GetIntermediaryPath ( - Path.Combine ("android", "bin", "classes", "com", "example")); - AssertSingleFile (classDirectory, "*Peer.class"); - - var acwMap = File.ReadAllText (builder.Output.GetIntermediaryPath ("acw-map.txt")); - StringAssert.Contains ("UnnamedProject.ComposedIdentifier;com.example.\u00c5Peer", acwMap); - StringAssert.Contains ("UnnamedProject.CanonicalEquivalentIdentifier;com.example.\u212bPeer", acwMap); - - var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); - Assert.IsFalse (DexUtils.ContainsClass ("Lcom/example/\u00c5Peer;", dexFile, AndroidSdkPath)); - Assert.IsTrue (DexUtils.ContainsClass ("Lcom/example/\u212bPeer;", dexFile, AndroidSdkPath)); - - static string AssertSingleFile (string directory, string pattern) - { - var files = Directory.GetFiles (directory, pattern, SearchOption.TopDirectoryOnly); - Assert.AreEqual ( - 1, - files.Length, - "APFS should expose the canonical-equivalent llvm-ir paths as one overwritten file."); - return files [0]; - } - - static bool CanonicalEquivalentPathsAlias (string directory) - { - var probeDirectory = Path.Combine (directory, "canonical-path-probe-" + Guid.NewGuid ().ToString ("N")); - Directory.CreateDirectory (probeDirectory); - try { - File.WriteAllText (Path.Combine (probeDirectory, "\u00c5"), "composed"); - File.WriteAllText (Path.Combine (probeDirectory, "\u212b"), "equivalent"); - return Directory.GetFiles (probeDirectory).Length == 1; - } finally { - foreach (var file in Directory.GetFiles (probeDirectory)) { - File.Delete (file); - } - Directory.Delete (probeDirectory); - } - } - } - - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters", "XA4205")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters", "XA4205")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters", "XA4205")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "void", "XA4208")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "void", "XA4208")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "void", "XA4208")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic", "XA4207")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic", "XA4207")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic", "XA4207")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters-and-void", "XA4205")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-parameters-and-void", "XA4207")] + Assert.IsFalse (builder.Build (proj), $"{runtime} should reject canonical-equivalent Java names."); + StringAssertEx.Contains ("error XA4258", builder.LastBuildOutput); + StringAssertEx.Contains ("\u212bPeer", builder.LastBuildOutput); + AssertNoUnicodeOutputs (builder, "\u00c5Peer"); + AssertNoUnicodeOutputs (builder, "\u212bPeer"); + } + + [TestCase (AndroidRuntime.CoreCLR, "parameters", "XA4205")] + [TestCase (AndroidRuntime.NativeAOT, "parameters", "XA4205")] + [TestCase (AndroidRuntime.CoreCLR, "void", "XA4208")] + [TestCase (AndroidRuntime.NativeAOT, "void", "XA4208")] + [TestCase (AndroidRuntime.CoreCLR, "generic", "XA4207")] + [TestCase (AndroidRuntime.NativeAOT, "generic", "XA4207")] + [TestCase (AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] + [TestCase (AndroidRuntime.NativeAOT, "parameters-and-void", "XA4205")] + [TestCase (AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] + [TestCase (AndroidRuntime.NativeAOT, "generic-parameters-and-void", "XA4207")] public void Build_InvalidExportField_ReportsLegacyDiagnostic ( - string typeMapImplementation, AndroidRuntime runtime, string invalidShape, string expectedCode) @@ -427,46 +280,40 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( "generic-parameters-and-void" => "public void InitialValue (int value) { }", _ => throw new InvalidOperationException ($"Unknown invalid [ExportField] shape '{invalidShape}'."), }; - var proj = CreateExportFieldValidationProject (runtime, typeMapImplementation, $""" + var proj = CreateExportFieldValidationProject (runtime, $""" [ExportField ("VALUE")] {initializer} """, genericType: invalidShape.StartsWith ("generic", StringComparison.Ordinal)); using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape} [ExportField] initializers."); + Assert.IsFalse (builder.Build (proj), $"{runtime} should reject {invalidShape} [ExportField] initializers."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); if (invalidShape == "parameters-and-void") { Assert.IsFalse ( builder.LastBuildOutput.Any (line => line.Contains ("error XA4208", StringComparison.Ordinal)), - "XA4205 should take precedence over XA4208, matching LLVM-IR." + "XA4205 should take precedence over XA4208." ); } else if (invalidShape == "generic-parameters-and-void") { Assert.IsFalse ( builder.LastBuildOutput.Any (line => line.Contains ("error XA4205", StringComparison.Ordinal) || line.Contains ("error XA4208", StringComparison.Ordinal)), - "XA4207 should take precedence over initializer signature diagnostics, matching LLVM-IR." + "XA4207 should take precedence over initializer signature diagnostics." ); } } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-parameter", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-return", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-return", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-return", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "arbitrary-field", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "arbitrary-field", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "arbitrary-field", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-parameter", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-instantiation", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-instantiation", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-instantiation", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "function-pointer", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "function-pointer", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "function-pointer", "XA4263")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "by-ref-parameter", "XA4263")] @@ -475,49 +322,34 @@ public void Build_InvalidExportField_ReportsLegacyDiagnostic ( [TestCase ("trimmable", AndroidRuntime.NativeAOT, "pointer-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "rectangular-array-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "rectangular-array-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-declaring-type", "XA4206")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-declaring-type", "XA4206")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-declaring-type", "XA4206")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "mismatched-export-parameter", "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "mismatched-export-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "mismatched-export-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-export-parameter", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-export-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-export-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "function-pointer-export-parameter", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "function-pointer-export-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "function-pointer-export-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "mismatched-field-export-parameter", "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "mismatched-field-export-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "mismatched-field-export-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-parameter", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-parameter", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-parameter", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-return", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-return", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-return", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-array-field", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-array-field", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-array-field", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "special-xml-array-return", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "special-xml-array-return", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "special-xml-array-return", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-static-constructor", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-static-constructor", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-static-constructor", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-arbitrary", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-arbitrary", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-arbitrary", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-invalid-kind", "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-invalid-kind", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-invalid-kind", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-constructor-valid-kind", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-named-constructor-arbitrary", "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-named-constructor-arbitrary", "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-named-constructor-arbitrary", "XA4263")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "export-named-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "export-named-constructor-valid-kind", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "export-named-constructor-valid-kind", "success")] public void Build_ExportSignature_MatchesRuntimeClassification ( @@ -759,7 +591,7 @@ public SignaturePeer () { builder.ThrowOnBuildFailure = false; var succeeded = builder.Build (proj); if (expectedCode == "success") { - Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should retain legacy build support for {invalidShape}."); + Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should support {invalidShape}."); return; } @@ -885,7 +717,6 @@ public SpecialMappingLookalikePeer ( static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( AndroidRuntime runtime, - string typeMapImplementation, string members, bool genericType = false) { @@ -897,7 +728,6 @@ static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( }, }; proj.SetRuntime (runtime); - proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); proj.Sources.Add (new BuildItem.Source ("ExportFieldValidation.cs") { TextContent = () => $$""" using Android.Runtime; @@ -924,22 +754,18 @@ public void FindExportOutputs_FindsEveryArtifactKind () var typemapDirectory = Path.Combine (root, "typemap"); var acwMapFile = Path.Combine (root, "acw-map.txt"); var trimmableJavaDirectory = Path.Combine (typemapDirectory, "java", "com", "example"); - var llvmIrJavaDirectory = Path.Combine (root, "android", "src", "com", "example"); Directory.CreateDirectory (trimmableJavaDirectory); - Directory.CreateDirectory (llvmIrJavaDirectory); var expected = new [] { Path.Combine (typemapDirectory, "_Example.TypeMap.dll"), Path.Combine (typemapDirectory, "_Microsoft.Android.TypeMaps.dll"), acwMapFile, Path.Combine (trimmableJavaDirectory, "TrimmablePeer.java"), - Path.Combine (llvmIrJavaDirectory, "LlvmIrPeer.java"), }; File.WriteAllBytes (expected [0], []); File.WriteAllBytes (expected [1], []); File.WriteAllText (expected [2], "Managed, Assembly;com/example/Peer"); File.WriteAllText (expected [3], "public int VALUE = InitialValue ();"); - File.WriteAllText (expected [4], "public int VALUE = InitialValue ();"); CollectionAssert.AreEquivalent ( expected, @@ -984,7 +810,6 @@ static string [] FindExportOutputs (string typemapDirectory, string acwMapFile, return outputs.ToArray (); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "JAVAC0000", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4262", "XA4258")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4262", "XA4258")] public void Build_IndependentConstructorAndJavaNameDiagnostics_AreBothReported ( @@ -1030,15 +855,9 @@ public class ReservedName : Java.Lang.Object { } Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should report both independent errors."); StringAssertEx.Contains ($"error {constructorCode}", builder.LastBuildOutput); StringAssertEx.Contains ($"error {javaNameCode}", builder.LastBuildOutput); - if (typeMapImplementation == "trimmable") { - AssertNoExportOutputs (builder, "InvalidConstructor"); - } else { - StringAssertEx.Contains ("InvalidConstructor.java", builder.LastBuildOutput); - StringAssertEx.Contains ("for.java", builder.LastBuildOutput); - } + AssertNoExportOutputs (builder, "InvalidConstructor"); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XALNS7003")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "success")] public void Build_ExplicitExportConstructorAttributeOrders_MatchLegacyPipeline ( @@ -1089,12 +908,11 @@ public ExportFirst (uint value) { } if (expectedCode == "success") { Assert.IsTrue (succeeded, $"{runtime}/{typeMapImplementation} should preserve explicit constructor metadata."); } else { - Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should retain measured legacy validation."); + Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject the unsupported constructor metadata."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); } } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.NativeAOT)] public void Build_ImplicitConstructorUsesCompatibleBaseJniSignature (string typeMapImplementation, AndroidRuntime runtime) @@ -1136,49 +954,35 @@ public IntBase (int value) {} }); using var builder = CreateApkBuilder (); - Assert.IsTrue (builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy JNI base compatibility."); + Assert.IsTrue (builder.Build (proj), $"{runtime}/{typeMapImplementation} should use a compatible JNI base constructor."); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "lambda", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "lambda", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parenthesized-lambda", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parenthesized-lambda", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "typed-lambda", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "typed-lambda", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "typed-lambda", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "literal-comma", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "literal-comma", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "literal-comma", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "method-reference", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-method-reference", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-method-reference", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-construction", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-construction", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-construction", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "nested-generic", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "nested-generic", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "instanceof-generic", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "instanceof-generic", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "comparison", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "comparison", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "comparison", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "shift", "success")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "shift", "success")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "shift", "success")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-bare", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-bare", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-bare", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-call", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-call", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-call", "XA4262")] - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "JAVAC0000")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "ordinary-arithmetic", "XA4262")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "ordinary-arithmetic", "XA4262")] public void Build_SuperArgumentsLambdaAndMethodReference_MatchJavac ( @@ -1277,13 +1081,10 @@ public static class SimpleEntry extends java.util.AbstractMap.SimpleEntry< } else { Assert.IsFalse (succeeded, $"{runtime}/{typeMapImplementation} should reject the bare p1 reference."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); - if (typeMapImplementation == "trimmable") { - AssertNoExportOutputs (builder, "SuperArgumentsPeer"); - } + AssertNoExportOutputs (builder, "SuperArgumentsPeer"); } } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "XALNS7004")] [TestCase ("trimmable", AndroidRuntime.CoreCLR, "XA4263")] [TestCase ("trimmable", AndroidRuntime.NativeAOT, "XA4263")] public void Build_UnsupportedExportConstructorOverloads_ReportOnlyExportDiagnostics ( @@ -1339,25 +1140,21 @@ public UnsupportedExportConstructorOverloads ( builder.ThrowOnBuildFailure = false; Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject unsupported exported constructors."); StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput); - if (typeMapImplementation == "trimmable") { - StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedOne'", builder.LastBuildOutput); - StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedTwo'", builder.LastBuildOutput); - Assert.IsFalse (builder.LastBuildOutput.Any (line => - line.Contains ("Type 'UnnamedProject.UnsupportedExportConstructorOverloads'", StringComparison.Ordinal) && - (line.Contains ("error XA4259", StringComparison.Ordinal) || - line.Contains ("error XA4260", StringComparison.Ordinal) || - line.Contains ("error XA4261", StringComparison.Ordinal)))); - } + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedOne'", builder.LastBuildOutput); + StringAssertEx.Contains ("unsupported signature type 'UnnamedProject.UnsupportedTwo'", builder.LastBuildOutput); + Assert.IsFalse (builder.LastBuildOutput.Any (line => + line.Contains ("Type 'UnnamedProject.UnsupportedExportConstructorOverloads'", StringComparison.Ordinal) && + (line.Contains ("error XA4259", StringComparison.Ordinal) || + line.Contains ("error XA4260", StringComparison.Ordinal) || + line.Contains ("error XA4261", StringComparison.Ordinal)))); AssertNoExportOutputs (builder, "UnsupportedExportConstructorOverloads"); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, true)] - [TestCase ("trimmable", AndroidRuntime.CoreCLR, false)] - [TestCase ("trimmable", AndroidRuntime.NativeAOT, false)] - public void Build_WithCollidingConstructorSignatures_MatchesLegacyCount ( + [TestCase ("trimmable", AndroidRuntime.CoreCLR)] + [TestCase ("trimmable", AndroidRuntime.NativeAOT)] + public void Build_WithCollidingConstructorSignatures_ReportsXA4259 ( string typeMapImplementation, - AndroidRuntime runtime, - bool shouldSucceed) + AndroidRuntime runtime) { bool isRelease = runtime == AndroidRuntime.NativeAOT; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -1387,11 +1184,7 @@ public ConstructorCollision (Kind value) { } using var builder = CreateApkBuilder (); builder.ThrowOnBuildFailure = false; - Assert.AreEqual (shouldSucceed, builder.Build (proj), $"{runtime}/{typeMapImplementation} should match legacy collision behavior."); - if (shouldSucceed) { - Assert.IsFalse (builder.LastBuildOutput.Any (line => line.Contains ("error XA4259", StringComparison.Ordinal))); - return; - } + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject colliding constructor signatures."); StringAssertEx.Contains ("error XA4259", builder.LastBuildOutput); var typemapDirectory = builder.Output.GetIntermediaryPath ("typemap"); diff --git a/tests/MSBuildDeviceIntegration/Tests/FastTimingTests.cs b/tests/MSBuildDeviceIntegration/Tests/FastTimingTests.cs index f1c048a864f..a17287d45ee 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastTimingTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastTimingTests.cs @@ -12,10 +12,9 @@ namespace Xamarin.Android.Build.Tests; public class FastTimingTests : DeviceTest { [Test] - public void ConcurrentEventsCanGrowAndDump () + public void TimingEventsCanDump () { const string completedMessage = "FAST_TIMING_EVENTS_COMPLETED"; - const string bufferGrowthMessage = "Allocated timing event buffer from 4096 to 8192"; const string dumpCompletedMessage = "[2/8] Assembly decompression"; const string timingFileName = "fast-timing.txt"; @@ -27,22 +26,11 @@ public void ConcurrentEventsCanGrowAndDump () var proj = new XamarinAndroidApplicationProject (packageName: packageName); proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetRuntimeIdentifiers ([DeviceAbi]); - proj.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); proj.SetProperty ("_AndroidFastTiming", "True"); proj.SetDefaultTargetDevice (); - proj.MainActivity = proj.DefaultMainActivity - .Replace ("//${USINGS}", "using System.Threading.Tasks;") - .Replace ( - "//${AFTER_ONCREATE}", - $$""" - Parallel.For (0, 8, _ => { - for (int i = 0; i < 1024; i++) { - Android.Runtime.JNIEnv.GetJniName (typeof (MainActivity)); - } - }); - Android.Util.Log.Info ("FastTimingTest", "{{completedMessage}}"); -""" - ); + proj.MainActivity = proj.DefaultMainActivity.Replace ( + "//${AFTER_ONCREATE}", + $"Android.Util.Log.Info (\"FastTimingTest\", \"{completedMessage}\");"); using var builder = CreateApkBuilder (packageName: packageName); Assert.IsTrue (builder.Install (proj), "Project should have installed."); @@ -54,19 +42,14 @@ public void ConcurrentEventsCanGrowAndDump () RunAdbCommand ($"shell setprop debug.dotnet.timing to-file,filename={timingFileName}"); ClearAdbLogcat (); - bool sawBufferGrowth = false; bool appCompleted = MonitorAdbLogcat ( - line => { - sawBufferGrowth |= line.Contains (bufferGrowthMessage, StringComparison.Ordinal); - return line.Contains (completedMessage, StringComparison.Ordinal); - }, + line => line.Contains (completedMessage, StringComparison.Ordinal), Path.Combine (Root, builder.ProjectDirectory, "fast-timing-events.log"), timeout: 60, onMonitoringStarted: () => StartActivityAndAssert (proj) ); Assert.IsTrue (appCompleted, $"Output did not contain {completedMessage}."); - Assert.IsTrue (sawBufferGrowth, $"Output did not contain {bufferGrowthMessage}."); RunAdbCommand ($"shell run-as {proj.PackageName} rm -f cache/{timingFileName}"); RunAdbCommand ( diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 803a2f87f2b..950367aace0 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -36,11 +36,6 @@ static IEnumerable Get_DotNetRun_Data () { var ret = new List (); - foreach (AndroidRuntime runtime in new[] { AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT }) { - AddTestData (true, "llvm-ir", runtime); - AddTestData (false, "llvm-ir", runtime); - } - AddTestData (true, "trimmable", AndroidRuntime.CoreCLR); AddTestData (false, "trimmable", AndroidRuntime.CoreCLR); AddTestData (true, "trimmable", AndroidRuntime.NativeAOT); @@ -65,10 +60,6 @@ public void DotNetRun (bool isRelease, string typemapImplementation, AndroidRunt return; } - if (runtime == AndroidRuntime.NativeAOT && typemapImplementation == "llvm-ir") { - Assert.Ignore ("NativeAOT doesn't work with LLVM-IR typemaps"); - } - var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { IsRelease = isRelease }; @@ -108,7 +99,6 @@ public void CoreCLRAssemblyNameWithNativeImageSuffix () StartActivityAndAssert (proj); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.NativeAOT)] public void UnicodeJavaIdentifierActivityActivates (string typeMapImplementation, AndroidRuntime runtime) @@ -1774,7 +1764,6 @@ public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet att Assert.IsTrue (didStart, "Activity should have started."); } - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.NativeAOT)] public void AppCompatJavaAliasCastsAndInflation ( @@ -2029,7 +2018,7 @@ public AppCompatImageButtonAlias (IntPtr handle, JniHandleOwnership transfer) } """.Replace ( "__ALLOW_REGISTERED_ALIAS_INFLATION__", - typemapImplementation == "llvm-ir" ? "true" : "false"); + "false"); using var builder = CreateApkBuilder (packageName: packageName); Assert.AreEqual (proj.PackageName, TestPackageNames [packageName], "Teardown should track the installed package."); RunAdbCommand ($"uninstall {proj.PackageName}"); @@ -2525,15 +2514,13 @@ public void TypeAndMemberRemapping ([Values] bool isRelease, [Values (AndroidRun static IEnumerable GetInterfaceMethodDesugaringData () { - foreach (var typemapImplementation in new [] { "llvm-ir", "trimmable" }) { - foreach (var useR8 in new [] { false, true }) { - foreach (var apiNative in new [] { true, false }) { - yield return CreateTestCase ( - typemapImplementation, - AndroidRuntime.CoreCLR, - apiNative, - useR8); - } + foreach (var useR8 in new [] { false, true }) { + foreach (var apiNative in new [] { true, false }) { + yield return CreateTestCase ( + "trimmable", + AndroidRuntime.CoreCLR, + apiNative, + useR8); } } diff --git a/tests/MSBuildDeviceIntegration/Tests/WebViewJavascriptInterfaceTests.cs b/tests/MSBuildDeviceIntegration/Tests/WebViewJavascriptInterfaceTests.cs index f6e5c8427e8..42f97cfedf8 100644 --- a/tests/MSBuildDeviceIntegration/Tests/WebViewJavascriptInterfaceTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/WebViewJavascriptInterfaceTests.cs @@ -15,7 +15,6 @@ public class WebViewJavascriptInterfaceTests : DeviceTest const string SuccessMarker = "# JAVASCRIPT_INTERFACE_RESULT "; const string FailureMarker = "# JAVASCRIPT_INTERFACE_FAILURE "; - [TestCase ("llvm-ir", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.CoreCLR)] [TestCase ("trimmable", AndroidRuntime.NativeAOT)] public void LocalJavascriptInvokesManagedBridge (string typemapImplementation, AndroidRuntime runtime) From 6caa22e6c4b720546166eee4b71ab338692136c4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 08:54:30 +0200 Subject: [PATCH 19/58] Remove legacy native typemap runtime support Keep trimmable managed mapping and JNI registration while dropping unused native typemap symbols, startup callbacks, and generated compatibility stubs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/app-bundles.md | 4 +- .../guides/internals/JavaJNI_Interop.md | 5 + .../JavaInteropRuntime.cs | 2 +- .../Java.Interop/JreRuntime.cs | 5 +- .../PreserveLists/Mono.Android.xml | 2 - .../Android.Runtime/AndroidRuntime.cs | 316 +---------- src/Mono.Android/Android.Runtime/JNIEnv.cs | 64 +-- .../Android.Runtime/JNIEnvInit.cs | 94 +-- .../Android.Runtime/RuntimeNativeMethods.cs | 9 - src/Mono.Android/Java.Interop/JavaConvert.cs | 21 +- src/Mono.Android/Java.Interop/TypeManager.cs | 420 +------------- .../JavaMarshalValueManager.cs | 151 ----- .../RuntimeFeature.cs | 10 - .../TrimmableTypeMap.cs | 4 +- .../TrimmableTypeMapValueManager.cs | 4 +- src/Mono.Android/Mono.Android.csproj | 1 - .../java/mono/android/TypeManager.java | 20 - .../Microsoft.Android.Sdk.CoreCLR.targets | 4 - ...soft.Android.Sdk.TypeMap.Trimmable.targets | 26 +- .../Tasks/BuildAppBundle.cs | 2 - .../Tasks/GenerateEmptyTypemapStub.cs | 84 --- .../GenerateNativeApplicationConfigSources.cs | 25 +- .../GenerateNativeMarshalMethodSources.cs | 183 ------ .../GenerateNativeRuntimeLinkingSources.cs | 113 ++++ .../Tasks/PrepareAbiItems.cs | 3 - .../Xamarin.Android.Build.Tests/BuildTest2.cs | 10 - .../IncrementalBuildTest.cs | 15 +- ...rateNativeApplicationConfigSourcesTests.cs | 4 + .../Tasks/LlvmIrGeneratorTests.cs | 20 - .../Utilities/EnvironmentHelper.cs | 51 +- .../Utilities/ApplicationConfig.cs | 2 - ...pplicationConfigNativeAssemblyGenerator.cs | 4 - .../MarshalMethodsNativeAssemblyGenerator.cs | 80 --- .../Xamarin.Android.Common.targets | 27 +- src/native/clr/host/CMakeLists.txt | 1 - src/native/clr/host/host.cc | 27 +- src/native/clr/host/internal-pinvokes-clr.cc | 19 - .../clr/host/internal-pinvokes-shared.cc | 1 - src/native/clr/host/typemap.cc | 533 ------------------ src/native/clr/include/host/host.hh | 2 - src/native/clr/include/host/typemap.hh | 40 -- .../include/runtime-base/internal-pinvokes.hh | 2 - src/native/clr/include/xamarin-app.hh | 87 --- .../clr/pinvoke-override/precompiled.cc | 6 - .../xamarin-app-stub/application_dso_stub.cc | 39 -- .../common/include/managed-interface.hh | 3 - .../common/runtime-base/timing-internal.cc | 12 - .../nativeaot/host/internal-pinvoke-stubs.cc | 16 - .../include/runtime-base/internal-pinvokes.hh | 2 - .../Android.Runtime/JnienvArrayMarshaling.cs | 9 - .../ActivationConstructorCacheTests.cs | 251 --------- .../ConstructorActivationTests.cs | 212 +------ .../Java.Interop/ExportTests.cs | 21 +- .../Java.Interop/InvokerActivationTests.cs | 14 - .../Java.Interop/JavaConvertTest.cs | 8 - .../Java.Interop/JnienvTest.cs | 10 +- .../Java.Interop/ManagedObjectProxyTests.cs | 23 +- .../TrimmableTypeMapRuntimeCoverageTests.cs | 21 - .../TrimmableTypeMapTypeManagerTests.cs | 48 -- .../Mono.Android.NET-Tests.csproj | 1 - .../TrustManagerMarshallingTests.cs | 10 - .../TestInstrumentation.cs | 15 +- 62 files changed, 206 insertions(+), 3012 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs delete mode 100644 src/Mono.Android/java/mono/android/TypeManager.java delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeRuntimeLinkingSources.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs delete mode 100644 src/native/clr/host/typemap.cc delete mode 100644 src/native/clr/include/host/typemap.hh delete mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ActivationConstructorCacheTests.cs diff --git a/Documentation/guides/app-bundles.md b/Documentation/guides/app-bundles.md index f34b91debc5..0a40cfa6d63 100644 --- a/Documentation/guides/app-bundles.md +++ b/Documentation/guides/app-bundles.md @@ -200,14 +200,14 @@ See the [.aab format spec][aab_format] for further detail. ## BundleConfig.json -Since .NET assemblies and typemap files must remain uncompressed in +Since .NET assemblies must remain uncompressed in .NET for Android apps, we will also need to specify a `BundleConfig.json` file: ```json { "compression": { - "uncompressedGlob": ["typemap.mj", "typemap.jm", "assemblies/*"] + "uncompressedGlob": ["assemblies/**"] } } ``` diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 2d6e8c82d72..5226edc2b1d 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,3 +1,8 @@ +> **Historical reference:** This guide describes the removed legacy LLVM IR/native +> typemap and marshal-method registration pipeline. Current CoreCLR and NativeAOT +> applications use the generated managed trimmable typemap and its Java callable +> wrappers instead. + - [Introduction](#introduction) - [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 0f51623426a..4f9d6269ffb 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -56,7 +56,7 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global), - TypeManager = JNIEnvInit.CreateTypeManager (initArgs), + TypeManager = JNIEnvInit.CreateTypeManager (), ValueManager = JNIEnvInit.CreateValueManager (), }; runtime = options.CreateJreVM (); diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 7e6de55cc6f..d7f50f035b2 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -80,11 +79,9 @@ static JniRuntime.JniTypeManager CreateDefaultTypeManager () return new TrimmableTypeMapTypeManager (); } - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] - [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] static JniRuntime.JniValueManager CreateDefaultValueManager () { - return new JavaMarshalValueManager (); + return new TrimmableTypeMapValueManager (); } public override string? GetCurrentManagedThreadName () diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml index b16215d73a3..64c49ba0016 100644 --- a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml @@ -10,7 +10,6 @@ - @@ -30,7 +29,6 @@ --> - diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 6f7231a97ce..aa7aa887010 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -1,16 +1,10 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Runtime.CompilerServices; -using System.Runtime.Versioning; using System.Threading; -using System.Reflection; using Java.Interop; -using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; -using System.Diagnostics.CodeAnalysis; using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; #if JAVA_INTEROP @@ -24,14 +18,12 @@ internal AndroidRuntime (IntPtr jnienv, IntPtr vm, IntPtr classLoader, JniRuntime.JniTypeManager typeManager, - JniRuntime.JniValueManager valueManager, - bool jniAddNativeMethodRegistrationAttributePresent) + JniRuntime.JniValueManager valueManager) : base (new AndroidRuntimeOptions (jnienv, vm, classLoader, typeManager, - valueManager, - jniAddNativeMethodRegistrationAttributePresent)) + valueManager)) { // This is not ideal, but we need to set this while the runtime is initializing but we can't do it directly from the `JNIEnvInit.Initialize` method, since // it lives in an assembly that does not reference Mono.Android. So we do it here, because this class is instantiated by JNIEnvInit.Initialize. @@ -106,8 +98,7 @@ public AndroidRuntimeOptions (IntPtr jnienv, IntPtr vm, IntPtr classLoader, JniRuntime.JniTypeManager typeManager, - JniRuntime.JniValueManager valueManager, - bool jniAddNativeMethodRegistrationAttributePresent) + JniRuntime.JniValueManager valueManager) { EnvironmentPointer = jnienv; ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global); @@ -115,7 +106,7 @@ public AndroidRuntimeOptions (IntPtr jnienv, ObjectReferenceManager = new ManagedObjectReferenceManager (); TypeManager = typeManager; ValueManager = valueManager; - JniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; + JniAddNativeMethodRegistrationAttributePresent = false; } } @@ -315,304 +306,5 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value) base.DeleteWeakGlobalReference (ref value); } } - - [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] - [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] - class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { - bool jniAddNativeMethodRegistrationAttributePresent; - - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) - { - this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; - } - - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) - { - foreach (var ti in base.GetTypesForSimpleReference (jniSimpleReference)) - yield return ti; - - var t = Java.Interop.TypeManager.GetJavaToManagedType (jniSimpleReference); - if (t != null) - yield return t; - } - - protected override Type? GetTypeForSimpleReference (string jniSimpleReference) - { - var type = base.GetTypeForSimpleReference (jniSimpleReference); - if (type != null) { - return type; - } - - return Java.Interop.TypeManager.GetJavaToManagedType (jniSimpleReference); - } - - protected override string? GetSimpleReference (Type type) - { - string? j = JNIEnv.TypemapManagedToJava (type); - if (j != null) { - return GetReplacementTypeCore (j) ?? j; - } - // Intentionally don't call base.GetSimpleReference(type): Android's - // non-trimmable runtime uses the generated/registered typemap, not - // Java.Interop's JniTypeSignatureAttribute fallback. - return null; - } - - protected override IEnumerable GetSimpleReferences (Type type) - { - string? j = JNIEnv.TypemapManagedToJava (type); - j = GetReplacementTypeCore (j) ?? j; - - if (j != null) { - return [j]; - } - // Keep this in sync with GetSimpleReference(): no base fallback. - return []; - } - - protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) - { - return JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); - } - - protected override string? GetReplacementTypeCore (string? jniSimpleReference) - { - return JniRemappingLookup.GetReplacementType (jniSimpleReference); - } - - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) - { - return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); - } - - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, ReadOnlySpan jniMethodName, ReadOnlySpan jniMethodSignature) - { - return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); - } - - protected override Type? GetInvokerTypeCore (Type type) - { - if (type.IsInterface || type.IsAbstract) { - return JavaObjectExtensions.GetInvokerType (type) - ?? base.GetInvokerTypeCore (type); - } - - return null; - } - - delegate Delegate GetCallbackHandler (); - - // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not - // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, - // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. - static readonly Lock prevent_delegate_gc_lock = new Lock (); - static readonly List prevent_delegate_gc = new List (); - static List sharedRegistrations = new List (); - - static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) - { - if (!MagicRegistrationMap.Filled) - return false; - - bool lockTaken = false; - bool rv = false; - - try { - Monitor.TryEnter (sharedRegistrations, ref lockTaken); - List registrations; - if (lockTaken) { - sharedRegistrations.Clear (); - registrations = sharedRegistrations; - } else { - registrations = new List (); - } - JniNativeMethodRegistrationArguments arguments = new JniNativeMethodRegistrationArguments (registrations, methods.ToString ()); - rv = MagicRegistrationMap.CallRegisterMethod (arguments, type.FullName!); - - if (registrations.Count > 0) - nativeClass.RegisterNativeMethods (registrations.ToArray ()); - } finally { - if (lockTaken) { - Monitor.Exit (sharedRegistrations); - } - } - - return rv; - } - - class MagicRegistrationMap { -#pragma warning disable CS0649 // Field is never assigned to; - // assigned to in generated IL: https://github.com/dotnet/android/blob/cbfa7e20acebd37b52ec4de9d5c1a4a66ddda799/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/MonoDroidMarkStep.cs#L204 - static Dictionary? typesMap; -#pragma warning restore CS0649 - - static void Prefill () - { - // fill code added by the linker - } - - static MagicRegistrationMap () - { - Prefill (); - } - - static public bool Filled { - get { - return typesMap != null && typesMap.Count > 0; - } - } - - internal static bool CallRegisterMethod (JniNativeMethodRegistrationArguments arguments, string typeName) - { - int idx = 0; - - if (typeName == null || !(typesMap?.TryGetValue (typeName, out idx) == true)) - return false; - - return CallRegisterMethodByIndex (arguments, idx); - } - - static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments arguments, int? typeIdx) - { - // updated by the linker to register known types - return false; - } - } - - [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan) instead.")] - public override void RegisterNativeMembers (JniType nativeClass, Type type, string? methods) => - RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); - - public override void RegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) - { - try { - if (methods.IsEmpty) { - if (jniAddNativeMethodRegistrationAttributePresent) { -#pragma warning disable CS0618 // ReflectionJniTypeManager has not migrated its registration override to spans. - base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); -#pragma warning restore CS0618 - } - return; - } else if (FastRegisterNativeMembers (nativeClass, type, methods)) { - return; - } - - int methodCount = CountMethods (methods); - if (methodCount < 1) { - if (jniAddNativeMethodRegistrationAttributePresent) { -#pragma warning disable CS0618 // ReflectionJniTypeManager has not migrated its registration override to spans. - base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); -#pragma warning restore CS0618 - } - return; - } - - JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; - int nativesIndex = 0; - MethodInfo []? typeMethods = null; - - ReadOnlySpan methodsSpan = methods; - bool needToRegisterNatives = false; - - while (!methodsSpan.IsEmpty) { - int newLineIndex = methodsSpan.IndexOf ('\n'); - - ReadOnlySpan methodLine = methodsSpan.Slice (0, newLineIndex != -1 ? newLineIndex : methodsSpan.Length); - if (!methodLine.IsEmpty) { - SplitMethodLine (methodLine, - out ReadOnlySpan name, - out ReadOnlySpan signature, - out ReadOnlySpan callbackString, - out ReadOnlySpan callbackDeclaringTypeString); - - Delegate? callback = null; - if (callbackString.SequenceEqual ("__export__")) { - var mname = name.Slice (2); - MethodInfo? minfo = null; - typeMethods ??= type.GetMethods (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - foreach (var mi in typeMethods) - if (mname.SequenceEqual (mi.Name) && signature.SequenceEqual (JavaNativeTypeManager.GetJniSignature (mi))) { - minfo = mi; - break; - } - - if (minfo == null) - throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); - - var exportAttribute = minfo.GetCustomAttribute () - ?? throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' does not have [Export] attribute. Signature: {signature.ToString ()}")); - - callback = exportAttribute.CreateDynamicCallback (minfo); - lock (prevent_delegate_gc_lock) { - prevent_delegate_gc.Add (callback); - } - needToRegisterNatives = true; - } else { - Type callbackDeclaringType = type; - if (!callbackDeclaringTypeString.IsEmpty) { - callbackDeclaringType = Type.GetType (callbackDeclaringTypeString.ToString (), throwOnError: true)!; - } - while (callbackDeclaringType.ContainsGenericParameters) { - callbackDeclaringType = callbackDeclaringType.BaseType!; - } - - GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), - callbackDeclaringType, callbackString.ToString ()); - callback = connector (); - } - - if (callback != null) { - needToRegisterNatives = true; - natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); - } - } - - methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; - } - - if (needToRegisterNatives) { - JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, nativesIndex); - } - } catch (Exception e) { - JniEnvironment.Runtime.RaisePendingException (e); - } - } - - static int CountMethods (ReadOnlySpan methodsSpan) - { - int count = 0; - while (!methodsSpan.IsEmpty) { - count++; - - int newLineIndex = methodsSpan.IndexOf ('\n'); - methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; - } - return count; - } - - static void SplitMethodLine ( - ReadOnlySpan methodLine, - out ReadOnlySpan name, - out ReadOnlySpan signature, - out ReadOnlySpan callback, - out ReadOnlySpan callbackDeclaringType) - { - int colonIndex = methodLine.IndexOf (':'); - name = methodLine.Slice (0, colonIndex); - methodLine = methodLine.Slice (colonIndex + 1); - - colonIndex = methodLine.IndexOf (':'); - signature = methodLine.Slice (0, colonIndex); - methodLine = methodLine.Slice (colonIndex + 1); - - colonIndex = methodLine.IndexOf (':'); - callback = methodLine.Slice (0, colonIndex != -1 ? colonIndex : methodLine.Length); - - callbackDeclaringType = colonIndex != -1 ? methodLine.Slice (colonIndex + 1) : default; - } - } - } #endif // JAVA_INTEROP diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index f1e42f311a3..841e0c96f8e 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; -using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; @@ -20,9 +19,6 @@ namespace Android.Runtime { public static partial class JNIEnv { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - [ThreadStatic] - static byte[]? mvid_bytes; - public static IntPtr Handle => JniEnvironment.EnvironmentPointer; static Array ArrayCreateInstance (Type elementType, int length) @@ -31,11 +27,7 @@ static Array ArrayCreateInstance (Type elementType, int length) return Array.CreateInstance (elementType, length); } - if (RuntimeFeature.TrimmableTypeMap) { - return SafeArrayFactory.CreateInstance (elementType, rank: 1, length); - } - - throw new NotSupportedException ($"It is not possible to create an array with element type '{elementType}'."); + return SafeArrayFactory.CreateInstance (elementType, rank: 1, length); } internal static IntPtr IdentityHash (IntPtr v) @@ -431,59 +423,9 @@ public static unsafe IntPtr NewObject (IntPtr jclass, IntPtr jmethod, params JVa return JniEnvironment.Types.GetJniTypeNameFromInstance (new JniObjectReference (jobject)); } - internal static void LogTypemapTrace (StackTrace st) + internal static string? TypemapManagedToJava (Type type) { - string? trace = st.ToString ()?.Trim (); - if (String.IsNullOrEmpty (trace)) - return; - - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Assembly, "typemap: called from"); - foreach (string line in trace!.Split ('\n')) { - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Assembly, line); - } - } - - internal static unsafe string? TypemapManagedToJava (Type type) - { - if (RuntimeFeature.TrimmableTypeMap) { - // The trimmable typemap doesn't use the native typemap tables. - // Delegate to the managed TrimmableTypeMap instead. - return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; - } - - byte[]? mvid_data = null; - // The Debug CoreCLR typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. - if (!RuntimeFeature.ManagedToJavaUsesAssemblyFullName) { - if (mvid_bytes == null) - mvid_bytes = new byte[16]; - - var mvid = new Span(mvid_bytes); - if (!type.Module.ModuleVersionId.TryWriteBytes (mvid)) { - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Failed to obtain module MVID using the fast method, falling back to the slow one"); - mvid_data = type.Module.ModuleVersionId.ToByteArray (); - } else { - mvid_data = mvid_bytes; - } - } - - if (type.FullName is null) - return null; - string? assemblyFullName = RuntimeFeature.ManagedToJavaUsesAssemblyFullName ? type.Assembly.FullName : null; - IntPtr ret; - fixed (byte* mvidptr = mvid_data) { - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); - } - - if (ret == IntPtr.Zero) { - if (Logger.LogAssembly) { - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"typemap: failed to map managed type to Java type: {type.AssemblyQualifiedName} (Module ID: {type.Module.ModuleVersionId}; Type token: {type.MetadataToken})"); - LogTypemapTrace (new StackTrace (true)); - } - - return null; - } - - return Marshal.PtrToStringAnsi (ret); + return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } public static string GetJniName (Type type) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 0048287ee5f..5ccf90f51ad 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -30,12 +29,10 @@ internal struct JnienvInitializeArgs { public byte brokenExceptionTransitions; public int packageNamingPolicy; public byte ioExceptionType; - public int jniAddNativeMethodRegistrationAttributePresent; public bool jniRemappingInUse; public bool marshalMethodsEnabled; public IntPtr grefGCUserPeerable; public IntPtr propagateUncaughtExceptionFn; - public IntPtr registerJniNativesFn; public IntPtr grefLogPath; public IntPtr lrefLogPath; public IntPtr referenceLogDirectory; @@ -66,36 +63,10 @@ static void PropagateUncaughtException (IntPtr env, IntPtr javaThread, IntPtr ja JNIEnv.PropagateUncaughtException (env, javaThread, javaException); } - [UnmanagedCallersOnly] - [RequiresUnreferencedCode ("Uses reflection to access System.StartupHookProvider.")] - static unsafe void RegisterJniNatives (IntPtr typeName_ptr, int typeName_len, IntPtr jniClass, IntPtr methods_ptr, int methods_len) - { - string typeName = new string ((char*) typeName_ptr, 0, typeName_len); - var type = Type.GetType (typeName, throwOnError: false); - if (type == null) { - RuntimeNativeMethods.monodroid_log (LogLevel.Error, - LogCategories.Default, - $"Could not load type '{typeName}'. Skipping JNI registration of type '{Java.Interop.TypeManager.GetClassName (jniClass)}'."); - return; - } - - var className = Java.Interop.TypeManager.GetClassName (jniClass); - Java.Interop.TypeManager.RegisterType (className, type); - - JniType? jniType = null; - JniType.GetCachedJniType (ref jniType, className); - - ReadOnlySpan methods = new ReadOnlySpan ((void*) methods_ptr, methods_len); - if (androidRuntime is null) { - throw new InvalidOperationException ("androidRuntime has not been initialized"); - } - androidRuntime.TypeManager.RegisterNativeMembers (jniType, type, methods); - } - internal static void InitializeBeforeRuntimeCreation (JnienvInitializeArgs args) { InitializeCommonState (args); - InitializeTrimmableTypeMapDataIfNeeded (); + TypeMapLoader.Initialize (); } internal static void InitializeMaxGrefCounts (JnienvInitializeArgs args) @@ -121,7 +92,7 @@ internal static void InitializeNativeAotRuntime (JniRuntime runtime, JnienvIniti } androidRuntime = runtime; JniRuntime.SetCurrent (runtime); - RegisterTrimmableTypeMapNativeMethodsIfNeeded (); + TrimmableTypeMap.RegisterNativeMethods (); SetSynchronizationContext (); } @@ -145,57 +116,32 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) InitializeBeforeRuntimeCreation (*args); - JniRuntime.JniTypeManager typeManager = CreateTypeManager (*args); + JniRuntime.JniTypeManager typeManager = CreateTypeManager (); JniRuntime.JniValueManager valueManager = CreateValueManager (); androidRuntime = new AndroidRuntime ( args->env, args->javaVm, args->grefLoader, typeManager, - valueManager, - args->jniAddNativeMethodRegistrationAttributePresent != 0 + valueManager ); JniRuntime.SetCurrent (androidRuntime); - RegisterTrimmableTypeMapNativeMethodsIfNeeded (); + TrimmableTypeMap.RegisterNativeMethods (); args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap) { - args->registerJniNativesFn = GetRegisterJniNativesFnPtr (); - } RunStartupHooksIfNeeded (); SetSynchronizationContext (); - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This method is never used with the trimmable type map.")] - IntPtr GetRegisterJniNativesFnPtr () => - (IntPtr)(delegate* unmanaged)&RegisterJniNatives; } - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "The AndroidTypeManager branch is only reached when RuntimeFeature.TrimmableTypeMap is false; the linker substitutes the feature switch and trims this branch in trimmable apps.")] - internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArgs args) + internal static JniRuntime.JniTypeManager CreateTypeManager () { - if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableTypeMapTypeManager (); - } - - return CreateAndroidTypeManager (args); - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This type manager won't be used in Native AOT builds.")] - [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This type manager won't be used in Native AOT builds.")] - static JniRuntime.JniTypeManager CreateAndroidTypeManager (JnienvInitializeArgs args) => new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); + return new TrimmableTypeMapTypeManager (); } internal static JniRuntime.JniValueManager CreateValueManager () { - if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableTypeMapValueManager (); - } - - return CreateJavaMarshalValueManager (); - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] - [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] - JniRuntime.JniValueManager CreateJavaMarshalValueManager () => new JavaMarshalValueManager (); + return new TrimmableTypeMapValueManager (); } static void InitializeCommonState (JnienvInitializeArgs args) @@ -223,30 +169,6 @@ static void InitializeCommonState (JnienvInitializeArgs args) JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args.packageNamingPolicy; } - static void InitializeTrimmableTypeMapDataIfNeeded () - { - if (RuntimeFeature.TrimmableTypeMap) { - InitializeTrimmableTypeMapData (); - } - } - - static void RegisterTrimmableTypeMapNativeMethodsIfNeeded () - { - if (RuntimeFeature.TrimmableTypeMap) { - // TypeMapLoader.Initialize() only loads managed typemap data. Registering - // mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader. - TrimmableTypeMap.RegisterNativeMethods (); - } - } - - // Separate method so the JIT doesn't try to resolve TypeMapLoader (from _Microsoft.Android.TypeMaps.dll) - // when compiling JNIEnvInit.Initialize() in non-trimmable builds where that assembly isn't present. - [MethodImpl (MethodImplOptions.NoInlining)] - static void InitializeTrimmableTypeMapData () - { - TypeMapLoader.Initialize (); - } - static void RunStartupHooksIfNeeded () { // Return if startup hooks are disabled or not CoreCLR diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index 14819922893..4083f34e538 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -123,15 +123,6 @@ internal static partial void _monodroid_register_reference_logging_callbacks ( [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial int _monodroid_max_gref_get (); - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr clr_typemap_managed_to_java (string fullName, string? assemblyFullName, IntPtr mvid); - - [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] - [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - [return: MarshalAs (UnmanagedType.U1)] - internal static partial bool clr_typemap_java_to_managed (string java_type_name, out IntPtr managed_assembly_name, out uint managed_type_token_id); - [LibraryImport (RuntimeConstants.InternalDllName)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] internal static partial delegate* unmanaged clr_initialize_gc_bridge ( diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 21724698937..5acc8105293 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -75,18 +75,14 @@ static class JavaConvert { return (h, t) => JNIEnv.GetArray (h, t, target.GetElementType ()); if (target.IsGenericType && !target.IsGenericTypeDefinition) { - if (RuntimeFeature.UseTypeMapAttributesForJavaDictionaryValueTypeLookups) { + if (RuntimeFeature.UseTypeMapAttributesForJavaDictionaryValueTypeLookups || + !System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { if (SafeJavaCollectionFactory.TryGetFromJniHandleConverter (target, out var collectionConverter)) return collectionConverter; - } else if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { + } else { var factoryConverter = TryMakeGenericCollectionTypeFactory (target); if (factoryConverter != null) return factoryConverter; - } else if (RuntimeFeature.TrimmableTypeMap) { - if (SafeJavaCollectionFactory.TryGetFromJniHandleConverter (target, out var collectionConverter)) - return collectionConverter; - } else { - throw new NotSupportedException ($"Cannot convert Java collection elements to closed generic array element type '{target}' because the runtime does not support dynamic code generation."); } } @@ -173,9 +169,7 @@ public ArrayElementConverter (Array array) return JNIEnv.GetArray (handle, transfer, elementType.GetElementType ()); if (elementType != null && typeof (IJavaPeerable).IsAssignableFrom (elementType)) { - if (RuntimeFeature.TrimmableTypeMap) - return FromJniHandleWithTrimmableTypeMapping (handle, transfer, elementType); - return GetObjectWithSuppression (handle, transfer, elementType); + return FromJniHandleWithTrimmableTypeMapping (handle, transfer, elementType); } var value = FromJniHandleWithRuntimeTypeMapping (handle, transfer); @@ -183,13 +177,6 @@ public ArrayElementConverter (Array array) return value; return Convert.ChangeType (value, elementType, CultureInfo.InvariantCulture); } - - [UnconditionalSuppressMessage ("ReflectionAnalysis", "IL2067:RequiresUnreferencedCode", - Justification = "Custom trimmer steps marks the activation constructors on IJavaPeerable types.")] - static object? GetObjectWithSuppression (IntPtr handle, JniHandleOwnership transfer, Type? elementType) - { - return Java.Lang.Object.GetObject (handle, transfer, elementType); - } } static object? FromJniHandleWithRuntimeTypeMapping (IntPtr handle, JniHandleOwnership transfer) diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 5e6f3ef85e1..ce6f6950492 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -1,17 +1,8 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading; -using Java.Interop.Tools.TypeNameMappings; using Android.Runtime; -using Microsoft.Android.Runtime; -using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; namespace Java.Interop { @@ -99,388 +90,6 @@ public int Compare (string? x, string? y) return mappings [i].Substring (c+1); } - static _JniMarshal_PPLLLL_V? cb_activate; - internal static Delegate GetActivateHandler () - { - return cb_activate ??= new _JniMarshal_PPLLLL_V (n_Activate); - } - -#if JAVA_INTEROP - internal static bool ActivationEnabled { - get { return !JniEnvironment.WithinNewObjectScope; } - } -#else // !JAVA_INTEROP - [ThreadStatic] - static bool activation_disabled; - - internal static bool ActivationEnabled { - get { return !activation_disabled; } - set { activation_disabled = !value; } - } -#endif // !JAVA_INTEROP - - [UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Type.GetType() can never statically know the string value from parameter 'signature'.")] - static Type[] GetParameterTypes (string? signature) - { - if (String.IsNullOrEmpty (signature)) - return Array.Empty (); - string[] typenames = signature!.Split (':'); - Type[] result = new Type [typenames.Length]; - for (int i = 0; i < typenames.Length; i++) - result [i] = Type.GetType (typenames [i], throwOnError:true)!; - return result; - } - - [global::System.Diagnostics.DebuggerDisableUserUnhandledExceptions] - [UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Type.GetType() can never statically know the string value from parameter 'typename_ptr'.")] - static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) - { - if (!global::Java.Interop.JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r)) - return; - - try { - var o = Java.Lang.Object.PeekObject (jobject); - var ex = o as IJavaPeerable; - if (ex != null) { - var state = ex.JniManagedPeerState; - if (!state.HasFlag (JniManagedPeerStates.Activatable) && !state.HasFlag (JniManagedPeerStates.Replaceable)) - return; - } - if (!ActivationEnabled) { - if (Logger.LogGlobalRef) { - Logger.Log (LogLevel.Info, "monodroid-gref", - FormattableString.Invariant ($"warning: Skipping managed constructor invocation for handle 0x{jobject:x} (key_handle 0x{JNIEnv.IdentityHash (jobject):x}). Please use JNIEnv.StartCreateInstance() + JNIEnv.FinishCreateInstance() instead of JNIEnv.NewObject() and/or JNIEnv.CreateInstance().")); - } - return; - } - - Type type = Type.GetType (JNIEnv.GetString (typename_ptr, JniHandleOwnership.DoNotTransfer)!, throwOnError:true)!; - if (type.IsGenericTypeDefinition) { - throw new NotSupportedException ( - "Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined.", - CreateJavaLocationException ()); - } - Type[] ptypes = GetParameterTypes (JNIEnv.GetString (signature_ptr, JniHandleOwnership.DoNotTransfer)); - var parms = JNIEnv.GetObjectArray (parameters_ptr, ptypes); - var cinfo = type.GetConstructor (ptypes); - if (cinfo == null) { - throw CreateMissingConstructorException (type, ptypes); - } - if (o != null) { - cinfo.Invoke (o, parms); - return; - } - - Activate (jobject, cinfo, parms); - } catch (global::System.Exception __e) { - __r.OnUserUnhandledException (ref __envp, __e); - return; - } finally { - global::Java.Interop.JniEnvironment.EndMarshalMethod (ref __envp); - } - } - - [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "RuntimeHelpers.GetUninitializedObject() does not statically know the return value from ConstructorInfo.DeclaringType.")] - internal static void Activate (IntPtr jobject, ConstructorInfo cinfo, object? []? parms) - { - try { - var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType!); - if (newobj is IJavaPeerable peer) { - peer.SetPeerReference (new JniObjectReference (jobject)); - } else { - throw new InvalidOperationException ($"Unsupported type: '{newobj}'"); - } - cinfo.Invoke (newobj, parms); - } catch (Exception e) { - var m = FormattableString.Invariant ( - $"Could not activate JNI Handle 0x{jobject:x} (key_handle 0x{JNIEnv.IdentityHash (jobject):x}) of Java type '{JNIEnv.GetClassNameFromInstance (jobject)}' as managed type '{cinfo?.DeclaringType?.FullName}'."); - Logger.Log (LogLevel.Warn, "monodroid", m); - Logger.Log (LogLevel.Warn, "monodroid", CreateJavaLocationException ().ToString ()); - - throw new NotSupportedException (m, e); - } - } - - static Exception CreateMissingConstructorException (Type type, Type[] ptypes) - { - var message = new System.Text.StringBuilder (); - message.Append ("Unable to find "); - if (ptypes.Length == 0) - message.Append ("the default constructor"); - else { - message.Append ("a constructor with signature (") - .Append (ptypes [0].FullName); - for (int i = 1; i < ptypes.Length; ++i) - message.Append (", ").Append (ptypes [i].FullName); - message.Append (")"); - } - message.Append (" on type ").Append (type.FullName) - .Append (". Please provide the missing constructor."); - return new NotSupportedException (message.ToString (), CreateJavaLocationException ()); - } - - static Exception CreateJavaLocationException () - { - using (var loc = new Java.Lang.Error ("Java callstack:")) - return new JavaLocationException (loc.ToString ()); - } - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Value of java_type_name isn't statically known.")] - static Type? clr_typemap_java_to_managed (string java_type_name) - { - bool result = RuntimeNativeMethods.clr_typemap_java_to_managed (java_type_name, out IntPtr managedAssemblyNamePointer, out uint managedTypeTokenId); - if (!result || managedAssemblyNamePointer == IntPtr.Zero) { - return null; - } - - string? managedAssemblyName = Marshal.PtrToStringAnsi (managedAssemblyNamePointer); - if (managedAssemblyName is null) { - return null; - } - - Assembly assembly = Assembly.Load (managedAssemblyName); - Type? ret = null; - foreach (Module module in assembly.Modules) { - ret = module.ResolveType ((int)managedTypeTokenId); - if (ret != null) { - break; - } - } - - if (Logger.LogAssembly) { - Logger.Log (LogLevel.Info, "monodroid", $"Loaded type: {ret}"); - } - - return ret; - } - - internal static Type? GetJavaToManagedType (string class_name) - { - lock (TypeManagerMapDictionaries.AccessLock) { - return GetJavaToManagedTypeCore (class_name); - } - } - - static Type? GetJavaToManagedTypeCore (string class_name) - { - if (TypeManagerMapDictionaries.JniToManaged.TryGetValue (class_name, out Type? type)) { - return type; - } - - if (RuntimeFeature.TrimmableTypeMap) { - throw new System.Diagnostics.UnreachableException ( - $"{nameof (TypeManager)}.{nameof (GetJavaToManagedTypeCore)} should not be used when " + - $"{nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. The trimmable path should resolve " + - $"types through {nameof (TrimmableTypeMapTypeManager)}."); - } else { - type = clr_typemap_java_to_managed (class_name); - } - - if (type != null) { - TypeManagerMapDictionaries.JniToManaged.Add (class_name, type); - return type; - } - - // Miss message is logged in the native runtime - if (Logger.LogAssembly) - JNIEnv.LogTypemapTrace (new System.Diagnostics.StackTrace (true)); - return null; - } - - [RequiresDynamicCode ("Legacy type manager peer creation can construct generic invoker types.")] - [RequiresUnreferencedCode ("Legacy type manager peer creation uses reflection over preserved Java peer types.")] - internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) - { - return CreateInstance (handle, transfer, null); - } - - [RequiresDynamicCode ("Legacy type manager peer creation can construct generic invoker types.")] - [RequiresUnreferencedCode ("Legacy type manager peer creation uses reflection over preserved Java peer types.")] - internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer, Type? targetType) - { - Type? type = null; - IntPtr class_ptr = JNIEnv.GetObjectClass (handle); - string? class_name = GetClassName (class_ptr); - lock (TypeManagerMapDictionaries.AccessLock) { - while (class_ptr != IntPtr.Zero) { - if (!string.IsNullOrEmpty (class_name)) { - type = GetJavaToManagedTypeCore (class_name); - if (type != null) { - break; - } - } - - IntPtr super_class_ptr = JNIEnv.GetSuperclass (class_ptr); - JNIEnv.DeleteLocalRef (class_ptr); - class_name = null; - class_ptr = super_class_ptr; - if (class_ptr != IntPtr.Zero) { - class_name = GetClassName (class_ptr); - } - } - } - - if (class_ptr != IntPtr.Zero) { - JNIEnv.DeleteLocalRef (class_ptr); - class_ptr = IntPtr.Zero; - } - - if (targetType != null && - (type == null || - !targetType.IsAssignableFrom (type))) { - type = targetType; - } - - if (type == null) { - class_name = JNIEnv.GetClassNameFromInstance (handle); - JNIEnv.DeleteRef (handle, transfer); - throw new NotSupportedException ( - FormattableString.Invariant ($"Internal error finding wrapper class for '{class_name}'. (Where is the Java.Lang.Object wrapper?!)"), - CreateJavaLocationException ()); - } - - if (type.IsInterface || type.IsAbstract) { - var invokerType = JavaObjectExtensions.GetInvokerType (type); - if (invokerType == null) - throw new NotSupportedException ("Unable to find Invoker for type '" + type.FullName + "'. Was it linked away?", - CreateJavaLocationException ()); - type = invokerType; - } - - var typeSig = JNIEnvInit.androidRuntime?.TypeManager.GetTypeSignature (type) ?? default; - if (!typeSig.IsValid || typeSig.SimpleReference == null) { - throw new ArgumentException ($"Could not determine Java type corresponding to `{type.AssemblyQualifiedName}`.", nameof (targetType)); - } - - JniObjectReference typeClass = default; - JniObjectReference handleClass = default; - try { - try { - typeClass = JniEnvironment.Types.FindClass (typeSig.SimpleReference); - } catch (Exception e) { - throw new ArgumentException ($"Could not find Java class `{typeSig.SimpleReference}`.", - nameof (targetType), - e); - } - - handleClass = JniEnvironment.Types.GetObjectClass (new JniObjectReference (handle)); - if (!JniEnvironment.Types.IsAssignableFrom (handleClass, typeClass)) { - if (Logger.LogAssembly) { - var message = $"Handle 0x{handle:x} is of type '{JNIEnv.GetClassNameFromInstance (handle)}' which is not assignable to '{typeSig.SimpleReference}'"; - Logger.Log (LogLevel.Debug, "monodroid-assembly", message); - } - if (RuntimeFeature.IsAssignableFromCheck) { - return null; - } - } - } finally { - JniObjectReference.Dispose (ref handleClass); - JniObjectReference.Dispose (ref typeClass); - } - - IJavaPeerable? result = null; - - try { - result = (IJavaPeerable) CreateProxy (type, handle, transfer); - if (Runtime.IsGCUserPeer (result.PeerReference.Handle)) { - result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - } - } catch (MissingMethodException e) { - var key_handle = JNIEnv.IdentityHash (handle); - JNIEnv.DeleteRef (handle, transfer); - throw new NotSupportedException (FormattableString.Invariant ( - $"Unable to activate instance of type {type} from native handle 0x{handle:x} (key_handle 0x{key_handle:x})."), e); - } - return result; - } - - static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - static readonly Type[] JIConstructorSignature = new Type [] { typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions) }; - static readonly ConcurrentDictionary ActivationConstructorCache = new ConcurrentDictionary (1, 3); - - enum ActivationConstructorKind - { - Missing, - XA, - JI, - } - - readonly record struct ActivationConstructor (ConstructorInfo? Constructor, ActivationConstructorKind Kind); - - /// - /// Preserves constructor annotations through the stateful GetOrAdd factory, - /// whose Func key parameter cannot carry . - /// - readonly struct AnnotatedType - { - public AnnotatedType ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - Type = type; - } - - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - public Type Type { get; } - } - - internal static object CreateProxy ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type, - IntPtr handle, - JniHandleOwnership transfer) - { - // Skip Activator.CreateInstance() as that requires public constructors, - // and we want to hide some constructors for sanity reasons. - var peer = GetUninitializedObject (type); - // XA constructors are the normal activation path; resolve XA first and cache the result. - var activation = GetActivationConstructor (type); - var constructor = activation.Constructor; - if (constructor != null && activation.Kind == ActivationConstructorKind.XA) { - constructor.Invoke (peer, new object[] { handle, transfer }); - return peer; - } - // JI constructors are a legacy fallback slated for removal, but cache them while supported. - if (constructor != null && activation.Kind == ActivationConstructorKind.JI) { - JniObjectReference r = new JniObjectReference (handle); - JniObjectReferenceOptions o = JniObjectReferenceOptions.Copy; - constructor.Invoke (peer, new object [] { r, o }); - JNIEnv.DeleteRef (handle, transfer); - return peer; - } - GC.SuppressFinalize (peer); - throw new MissingMethodException ( - "No constructor found for " + type.FullName + "::.ctor(System.IntPtr, Android.Runtime.JniHandleOwnership)", - CreateJavaLocationException ()); - - static ActivationConstructor GetActivationConstructor ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - return ActivationConstructorCache.GetOrAdd (type, - static (_, state) => { - const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - var constructor = state.Type.GetConstructor (flags, null, XAConstructorSignature, null); - if (constructor != null) - return new ActivationConstructor (constructor, ActivationConstructorKind.XA); - - constructor = state.Type.GetConstructor (flags, null, JIConstructorSignature, null); - return new ActivationConstructor ( - constructor, - constructor == null ? ActivationConstructorKind.Missing : ActivationConstructorKind.JI); - }, new AnnotatedType (type)); - } - - static IJavaPeerable GetUninitializedObject ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - var v = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type); - v.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - return v; - } - } - public static void RegisterType (string java_class, Type t) { string jniFromType = JNIEnv.GetJniName (t); @@ -499,11 +108,11 @@ public static void RegisterType (string java_class, Type t) } const string TypeRegistrationNotSupported = - "Java package type registration is no longer supported. Java-to-managed type resolution now goes through the native and trimmable type maps."; + "Java package type registration is no longer supported. Java-to-managed type resolution now goes through the trimmable type map."; // The package-based Java-to-managed type registration fallback was removed // (https://github.com/dotnet/android/issues/11663); type resolution now goes - // through the native / trimmable type map, and the generator no longer emits + // through the trimmable type map, and the generator no longer emits // the `Java.Interop.__TypeRegistrations` class that called these methods. These // shipped public APIs are retained for binary compatibility but now throw, as // they can no longer register anything. @@ -517,30 +126,5 @@ public static void RegisterPackages (string[] packages, Converter throw new NotSupportedException (TypeRegistrationNotSupported); } - [Register ("mono/android/TypeManager", DoNotGenerateAcw = true)] - internal class JavaTypeManager : Java.Lang.Object - { - [Register ("activate", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V", "")] - static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) - { - TypeManager.n_Activate (jnienv, jclass, typename_ptr, signature_ptr, jobject, parameters_ptr); - } - - [UnmanagedCallersOnly] - static void n_Activate_mm (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) - { - // TODO: need a full wrapper code here, a'la JNINativeWrapper.CreateDelegate - try { - TypeManager.n_Activate (jnienv, jclass, typename_ptr, signature_ptr, jobject, parameters_ptr); - } catch (Exception ex) { - AndroidEnvironment.UnhandledException (ex); - } - } - - internal static Delegate GetActivateHandler () - { - return TypeManager.GetActivateHandler (); - } - } } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs deleted file mode 100644 index 3f453c31882..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Android.Runtime; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -[RequiresDynamicCode ("This value manager is reflection-backed and can break in AOT scenarios.")] -[RequiresUnreferencedCode ("This value manager is reflection-backed and relies on custom trimming rules.")] -sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - static readonly Type[] JIConstructorSignature = new Type [] { typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions) }; - static readonly ConcurrentDictionary ActivationConstructorCache = new ConcurrentDictionary (1, 3); - - enum ActivationConstructorKind - { - Missing, - XA, - JI, - } - - readonly record struct ActivationConstructor (ConstructorInfo? Constructor, ActivationConstructorKind Kind); - - // The GetOrAdd factory's key parameter cannot carry constructor-preservation annotations. - readonly struct AnnotatedType - { - public AnnotatedType ([DynamicallyAccessedMembers (Constructors)] Type type) - { - Type = type; - } - - [DynamicallyAccessedMembers (Constructors)] - public Type Type { get; } - } - - public JavaMarshalValueManager () - { - JavaMarshalRegisteredPeers.InitializeIfNeeded (); - } - - public override void WaitForGCBridgeProcessing () - { - // Intentionally empty. The Mono runtime's own implementation acknowledges this - // pattern is fundamentally flawed (see FIXME in sgen-bridge.c): a thread that - // passes the check can still race with bridge processing that starts immediately - // after. The wait cannot prevent the race, only reduce its window. On CoreCLR, - // JNI wrapper threads hold their own handle copies via JniObjectReference, so - // they are not affected by the bridge swapping control_block handles. - } - - public override void CollectPeers () - { - JavaMarshalRegisteredPeers.CollectPeers (); - } - - public override void AddPeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.AddPeer (value); - } - - public override IJavaPeerable? PeekPeer (JniObjectReference reference) - { - EnsureNotDisposed (); - return JavaMarshalRegisteredPeers.PeekPeer (reference); - } - - public override void RemovePeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.RemovePeer (value); - } - - public override void FinalizePeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.FinalizePeer (value); - } - - public override List GetSurfacedPeers () - { - return JavaMarshalRegisteredPeers.GetSurfacedPeers (); - } - - protected override object? CreateNonArrayListValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - Type targetType) - { - return JavaConvert.FromObjectReference (ref reference, options, targetType); - } - - protected override bool TryConstructPeer ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - var activation = GetActivationConstructor (type); - var c = activation.Constructor; - if (c == null) - return false; - - if (activation.Kind == ActivationConstructorKind.XA) { - var args = new object[] { - reference.Handle, - JniHandleOwnership.DoNotTransfer, - }; - c.Invoke (self, args); - JniObjectReference.Dispose (ref reference, options); - return true; - } - - // Preserve ReflectionJniValueManager's JI fallback, including ref argument copy-back. - var jiArgs = new object[] { reference, options }; - c.Invoke (self, jiArgs); - reference = (JniObjectReference) jiArgs [0]; - JniObjectReference.Dispose (ref reference, options); - return true; - } - - static ActivationConstructor GetActivationConstructor ([DynamicallyAccessedMembers (Constructors)] Type type) - { - return ActivationConstructorCache.GetOrAdd (type, - static (_, state) => { - var constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (constructor != null) - return new ActivationConstructor (constructor, ActivationConstructorKind.XA); - - constructor = state.Type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); - return new ActivationConstructor ( - constructor, - constructor == null ? ActivationConstructorKind.Missing : ActivationConstructorKind.JI); - }, new AnnotatedType (type)); - } - - protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) - { - var proxy = value as JavaProxyThrowable; - if (proxy != null) { - result = proxy.InnerException; - return true; - } - return base.TryUnboxPeerObject (value, out result); - } -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 346ddf1399f..9ebc2fd70f1 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -10,10 +10,8 @@ static class RuntimeFeature const bool IsAssignableFromCheckEnabledByDefault = true; const bool StartupNoGCRegionEnabledByDefault = true; const bool StartupHookSupportEnabledByDefault = true; - const bool TrimmableTypeMapEnabledByDefault = false; const bool UseTypeMapAttributesForJavaDictionaryValueTypeLookupsEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; - const bool ManagedToJavaUsesAssemblyFullNameEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string EventSourceSupportSwitch = "System.Diagnostics.Tracing.EventSource.IsSupported"; @@ -40,10 +38,6 @@ static class RuntimeFeature internal static bool StartupHookSupport { get; } = AppContext.TryGetSwitch (StartupHookProviderSwitch, out bool isEnabled) ? isEnabled : StartupHookSupportEnabledByDefault; - [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (TrimmableTypeMap)}")] - internal static bool TrimmableTypeMap { get; } = - AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (TrimmableTypeMap)}", out bool isEnabled) ? isEnabled : TrimmableTypeMapEnabledByDefault; - [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (UseTypeMapAttributesForJavaDictionaryValueTypeLookups)}")] internal static bool UseTypeMapAttributesForJavaDictionaryValueTypeLookups { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (UseTypeMapAttributesForJavaDictionaryValueTypeLookups)}", out bool isEnabled) ? isEnabled : UseTypeMapAttributesForJavaDictionaryValueTypeLookupsEnabledByDefault; @@ -56,8 +50,4 @@ static class RuntimeFeature internal static bool EventSourceSupport { get; } = !AppContext.TryGetSwitch (EventSourceSupportSwitch, out bool isEnabled) || isEnabled; - // Enabled for Debug builds, whose string-based typemaps support Fast Deployment without embedding assembly MVIDs. - [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedToJavaUsesAssemblyFullName)}")] - internal static bool ManagedToJavaUsesAssemblyFullName { get; } = - AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedToJavaUsesAssemblyFullName)}", out bool isEnabled) ? isEnabled : ManagedToJavaUsesAssemblyFullNameEnabledByDefault; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca6be17f1c5..ef1c6b17d6e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -27,7 +27,7 @@ public class TrimmableTypeMap internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( - "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); + "TrimmableTypeMap has not been initialized. Ensure the JNI runtime is initialized."); readonly ITypeMap _typeMap; readonly ConcurrentDictionary _proxyCache = new (); @@ -94,7 +94,7 @@ internal static unsafe void RegisterNativeMethods () if (s_instance is null) { throw new InvalidOperationException ( - "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); + "TrimmableTypeMap has not been initialized. Ensure the JNI runtime is initialized."); } using var runtimeClass = new JniType ("mono/android/Runtime"u8); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index a5e4e230bfd..219ccf8ad13 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -63,7 +63,7 @@ public override List GetSurfacedPeers () public override void ActivatePeer (JniObjectReference reference, Type type, ConstructorInfo cinfo, object?[]? argumentValues) { - throw new PlatformNotSupportedException ("Activating Java peers through the value manager is not supported when TrimmableTypeMap is enabled."); + throw new PlatformNotSupportedException ("Activating Java peers through the value manager is not supported."); } protected override void ConstructPeerCore ( @@ -181,7 +181,7 @@ protected override void ConstructPeerCore ( throw new NotSupportedException ( $"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " + - $"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " + + $"with targetType '{targetName}'. " + $"This indicates a missing trimmable typemap proxy or association and should be fixed in the generator."); } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 3001ecf7dd3..183aaa6d818 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -368,7 +368,6 @@ - diff --git a/src/Mono.Android/java/mono/android/TypeManager.java b/src/Mono.Android/java/mono/android/TypeManager.java deleted file mode 100644 index 035dd91228b..00000000000 --- a/src/Mono.Android/java/mono/android/TypeManager.java +++ /dev/null @@ -1,20 +0,0 @@ -package mono.android; - -public class TypeManager { - - public static void Activate (String typeName, String sig, Object instance, Object[] parameterList) - { - n_activate (typeName, sig, instance, parameterList); - } - - private static native void n_activate (String typeName, String sig, Object instance, Object[] parameterList); - -//#MARSHAL_METHODS:START - do not remove or modify this line, it is required during application build - static { - String methods = - "n_activate:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/Object;)V:GetActivateHandler\n" + - ""; - mono.android.Runtime.register ("Java.Interop.TypeManager+JavaTypeManager, Mono.Android", TypeManager.class, methods); - } -//#MARSHAL_METHODS:END - do not remove or modify this line, it is required during application build -} diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets index dc9feb20cdf..9ea09c2d056 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.CoreCLR.targets @@ -35,10 +35,6 @@ This file contains the CoreCLR-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> - - @@ -81,8 +80,6 @@ - @@ -507,27 +504,6 @@ - - - - <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> - <_TypeMapAssemblySource Include="@(_TypeMapStubAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> - %(_TypeMapStubAbis.Identity) - - - - - - - - - -- [Introduction](#introduction) -- [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) - - [Java Callable Wrappers (JCW)](#java-callable-wrappers-jcw) -- [Registration](#registration) - - [Dynamic registration](#dynamic-registration) - - [Dynamic Java Callable Wrappers registration code](#dynamic-java-callable-wrappers-registration-code) - - [Dynamic Registration call sequence](#dynamic-registration-call-sequence) - - [Marshal methods](#marshal-methods) - - [Marshal Methods Java Callable Wrappers registration code](#marshal-methods-java-callable-wrappers-registration-code) - - [Marshal methods C# source code](#marshal-methods-c-source-code) - - [JNI requirements](#jni-requirements) - - [LLVM IR code generation](#llvm-ir-code-generation) - - [Assembly rewriting](#assembly-rewriting) - - [Wrappers for methods with non-blittable types](#wrappers-for-methods-with-non-blittable-types) - - [UnmanagedCallersOnly attribute](#unmanagedcallersonly-attribute) - - [Marshal Methods Registration call sequence](#marshal-methods-registration-call-sequence) - - -# Introduction - -At the core of `.NET for Android` is its ability to interoperate with -the Java/Kotlin APIs implemented in the Android system. To make it -work, it is necessary to "bridge" the two separate worlds of Java VM -(`ART` in the Android OS) and the Managed VM (`MonoVM`). Application -developers expect to be able to call native Android APIs and receive -calls (or react to events) from the Android side using code written in -one of the .NET managed languages. To make it work, `.NET for Android` -employs a number of techniques, both at build and at run time, which -are described in the sections below. - -This guide is meant to explain the technical implementation in a way -that is sufficient to understand the system without having to read the -actual source code. - -# Java <-> Managed interoperability overview - -Java VM and Managed VM are two entirely separate entities which -co-exist in the same process/application. Despite sharing the same -process resources, they don't "naturally" communicate with each other. -There is no direct way to call Java/Kotlin from .NET a'la the -`p/invoke` mechanism which allows calling native code APIs. Nor there -exists a way for Java/Kotlin code to invoke managed methods. To make -it possible, `.NET for Android` takes advantage of the Java's [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html) -(`Java Native Interface`), a mechanism that allows native code -(.NET managed code being "native" in this context) to register -implementations of Java methods, written outside the Java VM and in -languages other than Java/Kotlin (for instance in `C`, `C++` or -`Rust`). - -Such methods need to be appropriately declared in the Java code, for -instance: - -```java -class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Each native method is declared using the `native` keyword, and -whenever it is invoked from other Java code, the Java VM will use the -JNI to invoke the target method. - -Native methods can be registered either dynamically (by calling the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function) or "statically", by providing a native shared library -which exports a symbol with appropriate name which points to the -native function implementing the Java method. - -Both ways of registration are described in detail in the following -sections. - -## Java Callable Wrappers (JCW) - -`.NET for Android` wraps the entire Android API by generating -appropriate C# code which mirrors the Java/Kotlin code (classes, -interfaces, methods, properties etc). Each generated class that -corresponds to a Java/Kotlin type, is derived from the -`Java.Lang.Object` class (implemented in the `Mono.Android` assembly), -which marks it as a "Java interoperable type", meaning that it can -implement or override virtual Java methods. To make registration and -invoking of such methods possible, it is necessary to generate a Java -class which mirrors the Managed one and provides an entry point to -the Java <-> Managed transition. The Java classes are generated -during application (as well as `.NET for Android`) build and we call -them **Java Callable Wrappers** (or **JCW** for short). For instance, -the following managed class: - -```csharp -public class MainActivity : AppCompatActivity -{ - public override Android.Views.View? OnCreateView (Android.Views.View? parent, string name, Android.Content.Context context, Android.Util.IAttributeSet attrs) - { - return base.OnCreateView (parent, name, context, attrs); - } - - protected override void OnCreate (Bundle savedInstanceState) - { - base.OnCreate(savedInstanceState); - DoSomething (savedInstanceState); - } - - void DoSomething (Bundle bundle) - { - // do something with the bundle - } -} -``` - -overrides two Java virtual methods found in the `AppCompatActivity` -type: `OnCreateView` and `OnCreate`. The `DoSomething` method does -not correspond to any method found in the base Java type, and thus it -won't be included in the JCW. - -The Java Callable Wrapper generated for the above class would look as -follows (a few generated methods not relevant to the discussion have -been omitted for brevity): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Understanding the connection between Managed methods and their Java -counterparts is required in order to understand the registration -mechanisms described in sections found later in this document. The -[Dynamic registration](#dynamic-registration) section will expand on -this example in order to explain the details of how the Managed type -and its methods are registered with the Java VM. - -# Registration - -Both mechanisms of method registration rely on generation of [Java -Callable Wrappers](#java-callable-wrappers-jcw), with [Dynamic -registration](#dynamic-registration) requiring more code to be -generated so that the registration can be performed at the runtime. - -JCW are generated only for types that derive from -the `Java.Lang.Object` type. Finding such types is the task of the -Java.Interop's [`JavaTypeScanner`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaTypeScanner.cs), -which uses `Mono.Cecil` to read all the assemblies referenced by the -application and its libraries. The returned list of assemblies is -then used by a variety of tasks, JCW being only one -of them. - -After all types are found, -[`JavaCallableWrapperGenerator`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -is invoked in order to analyze each method in each type, looking for -those which override a virtual Java method and, thus, need to be -included in the wrapper class code. The generator optionally (if -[marshal methods](#marshal-methods) are enabled) passes each method to -an implementation of the -[`Java.Interop.Tools.JavaCallableWrappers.JavaCallableMethodClassifier`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -abstract class (which is -[`MarshalMethodsClassifier`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs) -in our case), to check whether the given method can be registered -statically. - -`JavaCallableWrapperGenerator` looks for methods decorated with the -`[Register]` attribute, which most frequently is created by invoking -its constructor with three parameters: - - 1. Java method name - 2. JNI method signature - 3. "Connector" method name - -The "connector" is a static method which creates a delegate that -subsequently allows calling of the native callback method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -The above code is actually generated in the `Android.App.Activity` -class while .NET for Android is built, from which our example -`MainActivity` eventually derives. - -What happens with the above code depends on the registration mechanism -and is described in the sections below. - -## Dynamic registration - -This registration mechanism has been used by `.NET for Android` since -the beginning and it will remain in use for the foreseeable future -when the application is built in the `Debug` configuration or when -[Marshal Methods](#marshal-methods) are turned off. - -### Dynamic Java Callable Wrappers registration code - -Building on the C# example shown in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ -/** @hide */ - public static final String __md_methods; - static { - __md_methods = - "n_onCreateView:(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;:GetOnCreateView_Landroid_view_View_Ljava_lang_String_Landroid_content_Context_Landroid_util_AttributeSet_Handler\n" + - "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" + - ""; - mono.android.Runtime.register ("HelloAndroid.MainActivity, HelloAndroid", MainActivity.class, __md_methods); - } - - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Code fragment which takes part in registration is the class's static -constructor. For each method registered for the type (that is, -implemented or overridden in the managed code), the JCW generator -outputs a single string which contains full information about the type -and method to register. Each such registration string is terminated -with the newline character and the entire sequence ends with an empty -string. Together, all the lines are concatenated and placed in the -`__md_methods` static variable. The `mono.android.Runtime.register` -method (see below for more details) is then invoked to register all -the methods. - -### Dynamic Registration call sequence - -All the "native" methods declared in the generated Java type are -registered when the type is constructed or accessed for the first -time. This is when the Java VM invokes the type's static constructor, -kicking off a sequence of calls that eventually ends with all the type -methods registered with JNI: - - 1. `mono.android.Runtime.register` is itself a native method, - declared in the - [`Runtime`](../../src/java-runtime/java/mono/android/Runtime.java) - class of .NET for Android's Java runtime code, and implemented in - the native .NET for Android - [runtime](../../src/monodroid/jni/monodroid-glue.cc) (the - `MonodroidRuntime::Java_mono_android_Runtime_register` method). - Purpose of this method is to prepare a call into the - .NET for Android managed runtime code, the - [`Android.Runtime.JNIEnv::RegisterJniNatives`](../../src/Mono.Android/Android.Runtime/JNIEnv.cs) - method. - 2. `Android.Runtime.JNIEnv::RegisterJniNatives` is passed name of - the managed type for which to register Java methods and uses .NET - reflection to load that type, followed by a call to cache the - type (via `RegisterType` method in the - [`TypeManager`](../../src/Mono.Android/Java.Interop/TypeManager.cs) - class) to end with a call to the - `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - method. - 3. `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - eventually calls the - `Java.Interop.JniEnvironment.Types::RegisterNatives` method which - first generates a delegate to the native callback method, using - `System.Reflection.Emit` (via the - [`Android.Runtime.JNINativeWrapper::CreateDelegate`](../../src/Mono.Android/Android.Runtime/JNINativeWrapper.cs) - method) and, eventually, invokes Java JNI's `RegisterNatives` - function, finally registering the native methods for a managed - type. - -The `System.Reflection.Emit` sequence mentioned in 3. above is among -the most costly operations, repeated for each registered method. - -Some more information about Java type registration can be found -[here](https://github.com/dotnet/android/wiki/Blueprint#java-type-registration). - -## Marshal methods - -The goal of marshal methods is to completely bypass the [dynamic -registration sequence](#dynamic-registration-call-sequence), replacing -it with native code generated and compiled during application build, -thus saving on the startup time of the application. - -Marshal methods registration mechanism takes advantage of the JNI -ability to look up implementations of `native` Java methods in actual -native (shared) libraries. Such symbols must have names that follow a -set of rules, so that JNI is able to properly locate them (details are -explained in the [JNI Requirements](#jni-requirements) section below). - -To achieve that, the marshal methods mechanism uses a number of -classes which [generate native](#llvm-ir-code-generation) code and -[modify assemblies](#assembly-rewriting) that contain the registered -methods. - -Current implementation of the marshal methods classifier recognizes -the "standard" method registration pattern, using the example of the -`OnCreate` method shown in [Registration](#registration) above. - -The standard pattern consists of: - - * the "connector" method, `GetOnCreate_Landroid_os_Bundle_Handler` - above - * the delegate backing field, `cb_onCreate_Landroid_os_Bundle_` - above - * the native callback method, `n_OnCreate_Landroid_os_Bundle_` above - * and the virtual target method which dispatches the call to the - actual object, `OnCreate` above. - -Whenever the classifier's `ShouldBeDynamicallyRegistered` method is -called, it is passed not only the method's declaring type, but also -the `Register` attribute instance which it then uses to check whether -the method being registered conforms to the "standard" registration -pattern shown above. The connector, native callback methods as well -as the backing field must be private and static in order for the -registered method to be considered as a candidate for static -registration. - -Registered methods which don't follow the "standard" pattern will be -registered dynamically. - -### Marshal Methods Java Callable Wrappers registration code - -Building on the C# example show in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Note that, compared to the code generated for the [dynamic -registration](#dynamic-java-callable-wrappers-registration-code) -mechanism, there is no static constructor while the rest -of the code remains exactly the same. - -### Marshal methods C# source code - -The marshal methods sections below will all refer to this code -fragment: - - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -### JNI requirements - -JNI -[specifies](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names) -a number of rules which govern how the native symbol name is -constructed, so that a mapping of object-oriented Java code (with its -package names/namespaces, class names and overloadable methods) into the essentially -"flat" procedural "namespace" of the lowest common denominator C code. - -The precise rules are outlined in the URL above, and their short -version is as follows: - - * Each symbol starts with the `Java_` prefix - * Next follows the mangled (see below) **fully qualified class - name** - * Next the `_` character serves as a separator before - * A mangled **method** name, which is optionally followed by - * Double underscore `__` and the mangled method argument signature - -"Mangling" is a way of encoding certain characters that are not -directly representable both in the source code and in the native -symbol name. The JNI specification allows for direct use of ASCII -letters (capital and lowercase) and digits, while all the other -characters are either represented by placeholders or encoded as 16-bit -hexadecimal Unicode character code (table copied from the JNI -specification for easier reference): - -| Escape sequence | Denotes | -|-----------------|------------------------------------------| -| _0XXXX | a Unicode character XXXX, all lower case | -| _1 | The `_` character | -| _2 | The `;` character in signatures | -| _3 | The `[` character in signatures | -| _ | The `.` or `/` characters | - -Generation of JNI symbol names is performed by the -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -class while generating the native function source code. - -JNI supports two forms of the native symbol name, as signalled in the -bullet list above - a short and a long one. The former is looked up -first by the Java VM, followed by the latter. The latter needs to be -used only for overloaded methods, which is what our generator does. - -### LLVM IR code generation - -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -uses the LLVM IR generator infrastructure to output both data and -executable code for all the marshal methods wrappers. It is not -necessary to understand the generated code unless one needs to modify -it, so this document only shows the equivalent C++ code which can -serve as a guide to understanding how the marshal method runtime -invocation works: - -```C++ -using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr); - -static get_function_pointer_fn get_function_pointer; - -void xamarin_app_init (get_function_pointer_fn fn) noexcept -{ - get_function_pointer = fn; -} - -using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState); -static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr; - -extern "C" JNIEXPORT void -JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept -{ - if (android_app_activity_on_create_bundle == nullptr) { - get_function_pointer ( - 16, // mono image index - 0, // class index - 0x0600055B, // method token - reinterpret_cast(android_app_activity_on_create_bundle) // target pointer - ); - } - - android_app_activity_on_create_bundle (env, klass, savedInstanceState); -} -``` - -The `xamarin_app_init` function is output only once and is called by -the `.NET for Android` runtime twice during application startup - once -to pass `get_function_pointer_fn` which does **not** use any locks (as -we know that until a certain point during startup we are in a single -lock, so no data access races can happen) and the other time just -before handing control over to the MonoVM, to pass pointer to -`get_function_pointer_fn` which **does** employ locking (since during -runtime it may very well happen that our generated Java native -functions will be called from different threads simultaneously). - -The `Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2` -function is a template which is repeated for each Java native -function, with each function having its own set of arguments and its -own callback backing field (`android_app_activity_on_create_bundle` -here). - -The `get_function_pointer` function takes as parameters indexes into a -couple of tables, one for `MonoImage*` pointers and the other for -`MonoClass*` pointers - both of which are generated by the -`MarshalMethodsNativeAssemblyGenerator` class at application build -time and allow for very fast lookup during run time. Target methods -are retrieved by their token value, within the specified `MonoImage*` -(essentially a pointer to managed assembly image in memory) and class. - -The method identified in such manner, **must** be decorated in the -managed code with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute (see [below](#unmanagedcallersonly-attribute) for more -details) so that it can be invoked directly, as if it was a native -method itself, with minimal managed marshaling overhead. - -### Assembly rewriting - -Please refer to the [C# code fragment](#marshal-methods-c-source-code) -above in order to understand this section. - -Managed assemblies (including `Mono.Android.dll`) which contain Java -types need to be usable in two contexts: with the "traditional" -dynamic registration and with marshal methods. Both of these -mechanisms, however, have different requirements. We cannot assume -that any assembly (either from .NET for Android or a third party nuget) -will have "marshal methods friendly" code and thus we need to make -sure that the code meets our requirements. - -We do it by reading each relevant assembly and modifying it by -altering the definition of the native callbacks and removing the -code that's no longer used by marshal methods. This task is performed -by the -[`MarshalMethodsAssemblyRewriter`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs) -invoked during application build after all the assemblies are linked -but **before** type maps are generated (as rewriting **will** alter -the method and potentially type tokens) - -The exact modifications we apply are: - - * Removal of the **connector backing field** - * Removal of the **connector method** - * Generation of a **native callback wrapper** method, which catches - and propagates unhandled exceptions thrown by the native callback - or the target method. This method is decorated with the - `[UnmanagedCallersOnly]` attribute and called directly from the - native code. - * Optionally, generate code in the **native callback wrapper** to handle - [non-blittable types](#wrappers-for-methods-with-non-blittable-types). - -All the modifications are performed with `Mono.Cecil`. - -After modifications, the assembly contains equivalent of the following -C# code for each marshal method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Native callback exception wrapper - [UnmanagedCallersOnly] - static void n_OnCreate_Landroid_os_Bundle__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - try { - n_OnCreate_Landroid_os_Bundle_ (jnienv, native__this, native_savedInstanceState) - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - } - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -#### Wrappers for methods with non-blittable types - -The -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute requires that all the argument types as well as the method -return type are -[blittable](https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types). - -Among these types is one that's commonly used by the managed classes -implementing Java methods: `bool`. Currently this is the **only** -non-blittable type we've encountered in bindings, so at this point it -is the only one supported by the assembly rewriter. - -Whenever we encounter a method with a non-blittable type, we must -generate a wrapper for it, so that we can decorate it with the -`[UnmanagedCallersOnly]` attribute. This is easier and less error -prone than modifying the native callback method's IL stream to -implement the necessary conversion. - -An example of such method is -`Android.Views.View.IOnTouchListener::OnTouch`: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} -``` - -As it returns a `bool` value, it needs a wrapper to cast the return -value properly. Each wrapper method retains the native callback method -name, but appends the `_mm_wrapper` suffix to it: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} - -[UnmanagedCallersOnly] -static byte n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - try { - return n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(jnienv, native__this, native_v, native_e) ? 1 : 0; - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - return default; - } -} -``` - -The wrapper's return statement uses the ternary operator to "cast" the -boolean value to `1` (for `true`) or `0` (for `false`) because the -value of `bool` across the managed runtime can take a range of values: - - * `0` for `false` - * `-1` or `1` for `true` - * `!= 0` for true - -Since the `bool` type in C# can be 1, 2 or 4 bytes long, we need to -cast it to some type of a known and static size. The managed type -`byte` was chosen as it corresponds to the Java/JNI `jboolean` type, -defined as an unsigned 8-bit type. - -Whenever an **argument** value needs to be converted between `byte` and -`bool`, we generate code that is equivalent of the `argument != 0` -comparison, for instance for the -`Android.Views.View.IOnFocusChangeListener::OnFocusChange` method: - -```csharp -[UnmanagedCallersOnly] -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, byte hasFocus) -{ - n_OnFocusChange_Landroid_view_View_Z (jnienv, native__this, native_v, hasFocus != 0); -} - -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, bool hasFocus) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - __this.OnFocusChange (v, hasFocus); -} -``` - -#### UnmanagedCallersOnly attribute - -Each marshal methods native callback method is decorated with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute, in order for us to be able to invoke the callback directly -from native code with minimal overhead compared to traditional -managed-from-native method calls (`mono_runtime_invoke`) - -### Marshal Methods Registration call sequence - -The sequence described in the [dynamic -registration sequence](#dynamic-registration-call-sequence) section -above is completely removed for the marshal methods. What remains -common for both dynamic and marshal methods registration, is the -resolution of the native function target done by the Java VM runtime. -In both cases the method declared in a Java class as `native` is -looked up by the Java VM when first JIT-ing the code. The difference -lies in the way this lookup is performed. - -Dynamic registration uses the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function at the runtime, which stores a pointer to the registered -method inside the structure which describes a Java class in the Java -VM. - -Marshal methods, however, don't register anything with the JNI, -instead they rely on the symbol lookup mechanism of the Java VM. -Whenever a call to `native` Java method is JIT-ed and it is not -registered previously using the `RegisterNatives` JNI function, Java -VM will proceed to look for symbols in the process runtime image (e.g. -using `dlopen` + `dlsym` calls on Unix) and, having found a matching -symbol, use pointer to it as the target of the `native` Java method -call. From d11bf2cebb4f98186a182111025bd3af97b35691 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:12:11 +0200 Subject: [PATCH 21/58] Restore Java JNI interop guide for replacement PR Preserve the original guide bytes here so the dependent documentation PR can present its trimmable-runtime rewrite as a side-by-side diff. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../guides/internals/JavaJNI_Interop.md | 771 ++++++++++++++++++ 1 file changed, 771 insertions(+) create mode 100644 Documentation/guides/internals/JavaJNI_Interop.md diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md new file mode 100644 index 00000000000..5226edc2b1d --- /dev/null +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -0,0 +1,771 @@ +> **Historical reference:** This guide describes the removed legacy LLVM IR/native +> typemap and marshal-method registration pipeline. Current CoreCLR and NativeAOT +> applications use the generated managed trimmable typemap and its Java callable +> wrappers instead. + + +- [Introduction](#introduction) +- [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) + - [Java Callable Wrappers (JCW)](#java-callable-wrappers-jcw) +- [Registration](#registration) + - [Dynamic registration](#dynamic-registration) + - [Dynamic Java Callable Wrappers registration code](#dynamic-java-callable-wrappers-registration-code) + - [Dynamic Registration call sequence](#dynamic-registration-call-sequence) + - [Marshal methods](#marshal-methods) + - [Marshal Methods Java Callable Wrappers registration code](#marshal-methods-java-callable-wrappers-registration-code) + - [Marshal methods C# source code](#marshal-methods-c-source-code) + - [JNI requirements](#jni-requirements) + - [LLVM IR code generation](#llvm-ir-code-generation) + - [Assembly rewriting](#assembly-rewriting) + - [Wrappers for methods with non-blittable types](#wrappers-for-methods-with-non-blittable-types) + - [UnmanagedCallersOnly attribute](#unmanagedcallersonly-attribute) + - [Marshal Methods Registration call sequence](#marshal-methods-registration-call-sequence) + + +# Introduction + +At the core of `.NET for Android` is its ability to interoperate with +the Java/Kotlin APIs implemented in the Android system. To make it +work, it is necessary to "bridge" the two separate worlds of Java VM +(`ART` in the Android OS) and the Managed VM (`MonoVM`). Application +developers expect to be able to call native Android APIs and receive +calls (or react to events) from the Android side using code written in +one of the .NET managed languages. To make it work, `.NET for Android` +employs a number of techniques, both at build and at run time, which +are described in the sections below. + +This guide is meant to explain the technical implementation in a way +that is sufficient to understand the system without having to read the +actual source code. + +# Java <-> Managed interoperability overview + +Java VM and Managed VM are two entirely separate entities which +co-exist in the same process/application. Despite sharing the same +process resources, they don't "naturally" communicate with each other. +There is no direct way to call Java/Kotlin from .NET a'la the +`p/invoke` mechanism which allows calling native code APIs. Nor there +exists a way for Java/Kotlin code to invoke managed methods. To make +it possible, `.NET for Android` takes advantage of the Java's [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html) +(`Java Native Interface`), a mechanism that allows native code +(.NET managed code being "native" in this context) to register +implementations of Java methods, written outside the Java VM and in +languages other than Java/Kotlin (for instance in `C`, `C++` or +`Rust`). + +Such methods need to be appropriately declared in the Java code, for +instance: + +```java +class MainActivity + extends androidx.appcompat.app.AppCompatActivity +{ + public void onCreate (android.os.Bundle p0) + { + n_onCreate (p0); + } + + private native void n_onCreate (android.os.Bundle p0); +} +``` + +Each native method is declared using the `native` keyword, and +whenever it is invoked from other Java code, the Java VM will use the +JNI to invoke the target method. + +Native methods can be registered either dynamically (by calling the +[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) +JNI function) or "statically", by providing a native shared library +which exports a symbol with appropriate name which points to the +native function implementing the Java method. + +Both ways of registration are described in detail in the following +sections. + +## Java Callable Wrappers (JCW) + +`.NET for Android` wraps the entire Android API by generating +appropriate C# code which mirrors the Java/Kotlin code (classes, +interfaces, methods, properties etc). Each generated class that +corresponds to a Java/Kotlin type, is derived from the +`Java.Lang.Object` class (implemented in the `Mono.Android` assembly), +which marks it as a "Java interoperable type", meaning that it can +implement or override virtual Java methods. To make registration and +invoking of such methods possible, it is necessary to generate a Java +class which mirrors the Managed one and provides an entry point to +the Java <-> Managed transition. The Java classes are generated +during application (as well as `.NET for Android`) build and we call +them **Java Callable Wrappers** (or **JCW** for short). For instance, +the following managed class: + +```csharp +public class MainActivity : AppCompatActivity +{ + public override Android.Views.View? OnCreateView (Android.Views.View? parent, string name, Android.Content.Context context, Android.Util.IAttributeSet attrs) + { + return base.OnCreateView (parent, name, context, attrs); + } + + protected override void OnCreate (Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + DoSomething (savedInstanceState); + } + + void DoSomething (Bundle bundle) + { + // do something with the bundle + } +} +``` + +overrides two Java virtual methods found in the `AppCompatActivity` +type: `OnCreateView` and `OnCreate`. The `DoSomething` method does +not correspond to any method found in the base Java type, and thus it +won't be included in the JCW. + +The Java Callable Wrapper generated for the above class would look as +follows (a few generated methods not relevant to the discussion have +been omitted for brevity): + +```java +public class MainActivity + extends androidx.appcompat.app.AppCompatActivity +{ + public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) + { + return n_onCreateView (p0, p1, p2, p3); + } + private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); + + public void onCreate (android.os.Bundle p0) + { + n_onCreate (p0); + } + private native void n_onCreate (android.os.Bundle p0); +} +``` + +Understanding the connection between Managed methods and their Java +counterparts is required in order to understand the registration +mechanisms described in sections found later in this document. The +[Dynamic registration](#dynamic-registration) section will expand on +this example in order to explain the details of how the Managed type +and its methods are registered with the Java VM. + +# Registration + +Both mechanisms of method registration rely on generation of [Java +Callable Wrappers](#java-callable-wrappers-jcw), with [Dynamic +registration](#dynamic-registration) requiring more code to be +generated so that the registration can be performed at the runtime. + +JCW are generated only for types that derive from +the `Java.Lang.Object` type. Finding such types is the task of the +Java.Interop's [`JavaTypeScanner`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaTypeScanner.cs), +which uses `Mono.Cecil` to read all the assemblies referenced by the +application and its libraries. The returned list of assemblies is +then used by a variety of tasks, JCW being only one +of them. + +After all types are found, +[`JavaCallableWrapperGenerator`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) +is invoked in order to analyze each method in each type, looking for +those which override a virtual Java method and, thus, need to be +included in the wrapper class code. The generator optionally (if +[marshal methods](#marshal-methods) are enabled) passes each method to +an implementation of the +[`Java.Interop.Tools.JavaCallableWrappers.JavaCallableMethodClassifier`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) +abstract class (which is +[`MarshalMethodsClassifier`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs) +in our case), to check whether the given method can be registered +statically. + +`JavaCallableWrapperGenerator` looks for methods decorated with the +`[Register]` attribute, which most frequently is created by invoking +its constructor with three parameters: + + 1. Java method name + 2. JNI method signature + 3. "Connector" method name + +The "connector" is a static method which creates a delegate that +subsequently allows calling of the native callback method: + +```csharp +public class MainActivity : AppCompatActivity +{ + // Connector backing field + static Delegate? cb_onCreate_Landroid_os_Bundle_; + + // Connector method + static Delegate GetOnCreate_Landroid_os_Bundle_Handler () + { + if (cb_onCreate_Landroid_os_Bundle_ == null) + cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); + return cb_onCreate_Landroid_os_Bundle_; + } + + // Native callback + static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + { + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); + __this.OnCreate (savedInstanceState); + } + + // Target method + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) + { + const string __id = "onCreate.(Landroid/os/Bundle;)V"; + try { + JniArgumentValue* __args = stackalloc JniArgumentValue [1]; + __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); + _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); + } finally { + global::System.GC.KeepAlive (savedInstanceState); + } + } +} +``` + +The above code is actually generated in the `Android.App.Activity` +class while .NET for Android is built, from which our example +`MainActivity` eventually derives. + +What happens with the above code depends on the registration mechanism +and is described in the sections below. + +## Dynamic registration + +This registration mechanism has been used by `.NET for Android` since +the beginning and it will remain in use for the foreseeable future +when the application is built in the `Debug` configuration or when +[Marshal Methods](#marshal-methods) are turned off. + +### Dynamic Java Callable Wrappers registration code + +Building on the C# example shown in the [Java Callable +Wrappers](#java-callable-wrappers-jcw) section, the following Java +code is generated (only the parts relevant to registration are shown): + +```java +public class MainActivity + extends androidx.appcompat.app.AppCompatActivity +{ +/** @hide */ + public static final String __md_methods; + static { + __md_methods = + "n_onCreateView:(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;:GetOnCreateView_Landroid_view_View_Ljava_lang_String_Landroid_content_Context_Landroid_util_AttributeSet_Handler\n" + + "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" + + ""; + mono.android.Runtime.register ("HelloAndroid.MainActivity, HelloAndroid", MainActivity.class, __md_methods); + } + + public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) + { + return n_onCreateView (p0, p1, p2, p3); + } + + private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); + + public void onCreate (android.os.Bundle p0) + { + n_onCreate (p0); + } + + private native void n_onCreate (android.os.Bundle p0); +} +``` + +Code fragment which takes part in registration is the class's static +constructor. For each method registered for the type (that is, +implemented or overridden in the managed code), the JCW generator +outputs a single string which contains full information about the type +and method to register. Each such registration string is terminated +with the newline character and the entire sequence ends with an empty +string. Together, all the lines are concatenated and placed in the +`__md_methods` static variable. The `mono.android.Runtime.register` +method (see below for more details) is then invoked to register all +the methods. + +### Dynamic Registration call sequence + +All the "native" methods declared in the generated Java type are +registered when the type is constructed or accessed for the first +time. This is when the Java VM invokes the type's static constructor, +kicking off a sequence of calls that eventually ends with all the type +methods registered with JNI: + + 1. `mono.android.Runtime.register` is itself a native method, + declared in the + [`Runtime`](../../src/java-runtime/java/mono/android/Runtime.java) + class of .NET for Android's Java runtime code, and implemented in + the native .NET for Android + [runtime](../../src/monodroid/jni/monodroid-glue.cc) (the + `MonodroidRuntime::Java_mono_android_Runtime_register` method). + Purpose of this method is to prepare a call into the + .NET for Android managed runtime code, the + [`Android.Runtime.JNIEnv::RegisterJniNatives`](../../src/Mono.Android/Android.Runtime/JNIEnv.cs) + method. + 2. `Android.Runtime.JNIEnv::RegisterJniNatives` is passed name of + the managed type for which to register Java methods and uses .NET + reflection to load that type, followed by a call to cache the + type (via `RegisterType` method in the + [`TypeManager`](../../src/Mono.Android/Java.Interop/TypeManager.cs) + class) to end with a call to the + `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` + method. + 3. `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` + eventually calls the + `Java.Interop.JniEnvironment.Types::RegisterNatives` method which + first generates a delegate to the native callback method, using + `System.Reflection.Emit` (via the + [`Android.Runtime.JNINativeWrapper::CreateDelegate`](../../src/Mono.Android/Android.Runtime/JNINativeWrapper.cs) + method) and, eventually, invokes Java JNI's `RegisterNatives` + function, finally registering the native methods for a managed + type. + +The `System.Reflection.Emit` sequence mentioned in 3. above is among +the most costly operations, repeated for each registered method. + +Some more information about Java type registration can be found +[here](https://github.com/dotnet/android/wiki/Blueprint#java-type-registration). + +## Marshal methods + +The goal of marshal methods is to completely bypass the [dynamic +registration sequence](#dynamic-registration-call-sequence), replacing +it with native code generated and compiled during application build, +thus saving on the startup time of the application. + +Marshal methods registration mechanism takes advantage of the JNI +ability to look up implementations of `native` Java methods in actual +native (shared) libraries. Such symbols must have names that follow a +set of rules, so that JNI is able to properly locate them (details are +explained in the [JNI Requirements](#jni-requirements) section below). + +To achieve that, the marshal methods mechanism uses a number of +classes which [generate native](#llvm-ir-code-generation) code and +[modify assemblies](#assembly-rewriting) that contain the registered +methods. + +Current implementation of the marshal methods classifier recognizes +the "standard" method registration pattern, using the example of the +`OnCreate` method shown in [Registration](#registration) above. + +The standard pattern consists of: + + * the "connector" method, `GetOnCreate_Landroid_os_Bundle_Handler` + above + * the delegate backing field, `cb_onCreate_Landroid_os_Bundle_` + above + * the native callback method, `n_OnCreate_Landroid_os_Bundle_` above + * and the virtual target method which dispatches the call to the + actual object, `OnCreate` above. + +Whenever the classifier's `ShouldBeDynamicallyRegistered` method is +called, it is passed not only the method's declaring type, but also +the `Register` attribute instance which it then uses to check whether +the method being registered conforms to the "standard" registration +pattern shown above. The connector, native callback methods as well +as the backing field must be private and static in order for the +registered method to be considered as a candidate for static +registration. + +Registered methods which don't follow the "standard" pattern will be +registered dynamically. + +### Marshal Methods Java Callable Wrappers registration code + +Building on the C# example show in the [Java Callable +Wrappers](#java-callable-wrappers-jcw) section, the following Java +code is generated (only the parts relevant to registration are shown): + +```java +public class MainActivity + extends androidx.appcompat.app.AppCompatActivity +{ + public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) + { + return n_onCreateView (p0, p1, p2, p3); + } + + private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); + + public void onCreate (android.os.Bundle p0) + { + n_onCreate (p0); + } + + private native void n_onCreate (android.os.Bundle p0); +} +``` + +Note that, compared to the code generated for the [dynamic +registration](#dynamic-java-callable-wrappers-registration-code) +mechanism, there is no static constructor while the rest +of the code remains exactly the same. + +### Marshal methods C# source code + +The marshal methods sections below will all refer to this code +fragment: + + +```csharp +public class MainActivity : AppCompatActivity +{ + // Connector backing field + static Delegate? cb_onCreate_Landroid_os_Bundle_; + + // Connector method + static Delegate GetOnCreate_Landroid_os_Bundle_Handler () + { + if (cb_onCreate_Landroid_os_Bundle_ == null) + cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); + return cb_onCreate_Landroid_os_Bundle_; + } + + // Native callback + static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + { + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); + __this.OnCreate (savedInstanceState); + } + + // Target method + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) + { + const string __id = "onCreate.(Landroid/os/Bundle;)V"; + try { + JniArgumentValue* __args = stackalloc JniArgumentValue [1]; + __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); + _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); + } finally { + global::System.GC.KeepAlive (savedInstanceState); + } + } +} +``` + +### JNI requirements + +JNI +[specifies](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names) +a number of rules which govern how the native symbol name is +constructed, so that a mapping of object-oriented Java code (with its +package names/namespaces, class names and overloadable methods) into the essentially +"flat" procedural "namespace" of the lowest common denominator C code. + +The precise rules are outlined in the URL above, and their short +version is as follows: + + * Each symbol starts with the `Java_` prefix + * Next follows the mangled (see below) **fully qualified class + name** + * Next the `_` character serves as a separator before + * A mangled **method** name, which is optionally followed by + * Double underscore `__` and the mangled method argument signature + +"Mangling" is a way of encoding certain characters that are not +directly representable both in the source code and in the native +symbol name. The JNI specification allows for direct use of ASCII +letters (capital and lowercase) and digits, while all the other +characters are either represented by placeholders or encoded as 16-bit +hexadecimal Unicode character code (table copied from the JNI +specification for easier reference): + +| Escape sequence | Denotes | +|-----------------|------------------------------------------| +| _0XXXX | a Unicode character XXXX, all lower case | +| _1 | The `_` character | +| _2 | The `;` character in signatures | +| _3 | The `[` character in signatures | +| _ | The `.` or `/` characters | + +Generation of JNI symbol names is performed by the +[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) +class while generating the native function source code. + +JNI supports two forms of the native symbol name, as signalled in the +bullet list above - a short and a long one. The former is looked up +first by the Java VM, followed by the latter. The latter needs to be +used only for overloaded methods, which is what our generator does. + +### LLVM IR code generation + +[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) +uses the LLVM IR generator infrastructure to output both data and +executable code for all the marshal methods wrappers. It is not +necessary to understand the generated code unless one needs to modify +it, so this document only shows the equivalent C++ code which can +serve as a guide to understanding how the marshal method runtime +invocation works: + +```C++ +using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr); + +static get_function_pointer_fn get_function_pointer; + +void xamarin_app_init (get_function_pointer_fn fn) noexcept +{ + get_function_pointer = fn; +} + +using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState); +static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr; + +extern "C" JNIEXPORT void +JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept +{ + if (android_app_activity_on_create_bundle == nullptr) { + get_function_pointer ( + 16, // mono image index + 0, // class index + 0x0600055B, // method token + reinterpret_cast(android_app_activity_on_create_bundle) // target pointer + ); + } + + android_app_activity_on_create_bundle (env, klass, savedInstanceState); +} +``` + +The `xamarin_app_init` function is output only once and is called by +the `.NET for Android` runtime twice during application startup - once +to pass `get_function_pointer_fn` which does **not** use any locks (as +we know that until a certain point during startup we are in a single +lock, so no data access races can happen) and the other time just +before handing control over to the MonoVM, to pass pointer to +`get_function_pointer_fn` which **does** employ locking (since during +runtime it may very well happen that our generated Java native +functions will be called from different threads simultaneously). + +The `Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2` +function is a template which is repeated for each Java native +function, with each function having its own set of arguments and its +own callback backing field (`android_app_activity_on_create_bundle` +here). + +The `get_function_pointer` function takes as parameters indexes into a +couple of tables, one for `MonoImage*` pointers and the other for +`MonoClass*` pointers - both of which are generated by the +`MarshalMethodsNativeAssemblyGenerator` class at application build +time and allow for very fast lookup during run time. Target methods +are retrieved by their token value, within the specified `MonoImage*` +(essentially a pointer to managed assembly image in memory) and class. + +The method identified in such manner, **must** be decorated in the +managed code with the +[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) +attribute (see [below](#unmanagedcallersonly-attribute) for more +details) so that it can be invoked directly, as if it was a native +method itself, with minimal managed marshaling overhead. + +### Assembly rewriting + +Please refer to the [C# code fragment](#marshal-methods-c-source-code) +above in order to understand this section. + +Managed assemblies (including `Mono.Android.dll`) which contain Java +types need to be usable in two contexts: with the "traditional" +dynamic registration and with marshal methods. Both of these +mechanisms, however, have different requirements. We cannot assume +that any assembly (either from .NET for Android or a third party nuget) +will have "marshal methods friendly" code and thus we need to make +sure that the code meets our requirements. + +We do it by reading each relevant assembly and modifying it by +altering the definition of the native callbacks and removing the +code that's no longer used by marshal methods. This task is performed +by the +[`MarshalMethodsAssemblyRewriter`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs) +invoked during application build after all the assemblies are linked +but **before** type maps are generated (as rewriting **will** alter +the method and potentially type tokens) + +The exact modifications we apply are: + + * Removal of the **connector backing field** + * Removal of the **connector method** + * Generation of a **native callback wrapper** method, which catches + and propagates unhandled exceptions thrown by the native callback + or the target method. This method is decorated with the + `[UnmanagedCallersOnly]` attribute and called directly from the + native code. + * Optionally, generate code in the **native callback wrapper** to handle + [non-blittable types](#wrappers-for-methods-with-non-blittable-types). + +All the modifications are performed with `Mono.Cecil`. + +After modifications, the assembly contains equivalent of the following +C# code for each marshal method: + +```csharp +public class MainActivity : AppCompatActivity +{ + // Native callback + static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + { + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); + __this.OnCreate (savedInstanceState); + } + + // Native callback exception wrapper + [UnmanagedCallersOnly] + static void n_OnCreate_Landroid_os_Bundle__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) + { + try { + n_OnCreate_Landroid_os_Bundle_ (jnienv, native__this, native_savedInstanceState) + } catch (Exception ex) { + Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); + } + } + + // Target method + [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] + protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) + { + const string __id = "onCreate.(Landroid/os/Bundle;)V"; + try { + JniArgumentValue* __args = stackalloc JniArgumentValue [1]; + __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); + _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); + } finally { + global::System.GC.KeepAlive (savedInstanceState); + } + } +} +``` + +#### Wrappers for methods with non-blittable types + +The +[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) +attribute requires that all the argument types as well as the method +return type are +[blittable](https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types). + +Among these types is one that's commonly used by the managed classes +implementing Java methods: `bool`. Currently this is the **only** +non-blittable type we've encountered in bindings, so at this point it +is the only one supported by the assembly rewriter. + +Whenever we encounter a method with a non-blittable type, we must +generate a wrapper for it, so that we can decorate it with the +`[UnmanagedCallersOnly]` attribute. This is easier and less error +prone than modifying the native callback method's IL stream to +implement the necessary conversion. + +An example of such method is +`Android.Views.View.IOnTouchListener::OnTouch`: + +```csharp +static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) +{ + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); + var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); + bool __ret = __this.OnTouch (v, e); + return __ret; +} +``` + +As it returns a `bool` value, it needs a wrapper to cast the return +value properly. Each wrapper method retains the native callback method +name, but appends the `_mm_wrapper` suffix to it: + +```csharp +static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) +{ + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); + var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); + bool __ret = __this.OnTouch (v, e); + return __ret; +} + +[UnmanagedCallersOnly] +static byte n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) +{ + try { + return n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(jnienv, native__this, native_v, native_e) ? 1 : 0; + } catch (Exception ex) { + Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); + return default; + } +} +``` + +The wrapper's return statement uses the ternary operator to "cast" the +boolean value to `1` (for `true`) or `0` (for `false`) because the +value of `bool` across the managed runtime can take a range of values: + + * `0` for `false` + * `-1` or `1` for `true` + * `!= 0` for true + +Since the `bool` type in C# can be 1, 2 or 4 bytes long, we need to +cast it to some type of a known and static size. The managed type +`byte` was chosen as it corresponds to the Java/JNI `jboolean` type, +defined as an unsigned 8-bit type. + +Whenever an **argument** value needs to be converted between `byte` and +`bool`, we generate code that is equivalent of the `argument != 0` +comparison, for instance for the +`Android.Views.View.IOnFocusChangeListener::OnFocusChange` method: + +```csharp +[UnmanagedCallersOnly] +static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, byte hasFocus) +{ + n_OnFocusChange_Landroid_view_View_Z (jnienv, native__this, native_v, hasFocus != 0); +} + +static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, bool hasFocus) +{ + var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; + var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); + __this.OnFocusChange (v, hasFocus); +} +``` + +#### UnmanagedCallersOnly attribute + +Each marshal methods native callback method is decorated with the +[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) +attribute, in order for us to be able to invoke the callback directly +from native code with minimal overhead compared to traditional +managed-from-native method calls (`mono_runtime_invoke`) + +### Marshal Methods Registration call sequence + +The sequence described in the [dynamic +registration sequence](#dynamic-registration-call-sequence) section +above is completely removed for the marshal methods. What remains +common for both dynamic and marshal methods registration, is the +resolution of the native function target done by the Java VM runtime. +In both cases the method declared in a Java class as `native` is +looked up by the Java VM when first JIT-ing the code. The difference +lies in the way this lookup is performed. + +Dynamic registration uses the +[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) +JNI function at the runtime, which stores a pointer to the registered +method inside the structure which describes a Java class in the Java +VM. + +Marshal methods, however, don't register anything with the JNI, +instead they rely on the symbol lookup mechanism of the Java VM. +Whenever a call to `native` Java method is JIT-ed and it is not +registered previously using the `RegisterNatives` JNI function, Java +VM will proceed to look for symbols in the process runtime image (e.g. +using `dlopen` + `dlsym` calls on Unix) and, having found a matching +symbol, use pointer to it as the target of the `native` Java method +call. From 5954a0dcc1a04c5b8eff7c63a62d564abb76268d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:35:06 +0200 Subject: [PATCH 22/58] Leave JNI interoperability guide for documentation PR Keep this runtime PR byte-identical to its updated parent for the guide so the dependent documentation PR shows the complete replacement. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 5226edc2b1d..2d6e8c82d72 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,8 +1,3 @@ -> **Historical reference:** This guide describes the removed legacy LLVM IR/native -> typemap and marshal-method registration pipeline. Current CoreCLR and NativeAOT -> applications use the generated managed trimmable typemap and its Java callable -> wrappers instead. - - [Introduction](#introduction) - [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) From 6f948cd25593f421b4bd503c8e2d3952f23a8cae Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:45:56 +0200 Subject: [PATCH 23/58] Instantiate the trimmable type and value managers directly Both CoreCLR and NativeAOT now have a single supported manager pair, so remove the redundant JNIEnvInit factory methods. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 4 ++-- src/Mono.Android/Android.Runtime/JNIEnvInit.cs | 16 ++-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 4f9d6269ffb..b4daa017595 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -56,8 +56,8 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global), - TypeManager = JNIEnvInit.CreateTypeManager (), - ValueManager = JNIEnvInit.CreateValueManager (), + TypeManager = new TrimmableTypeMapTypeManager (), + ValueManager = new TrimmableTypeMapValueManager (), }; runtime = options.CreateJreVM (); diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 5ccf90f51ad..69adaef15f8 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -116,14 +116,12 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) InitializeBeforeRuntimeCreation (*args); - JniRuntime.JniTypeManager typeManager = CreateTypeManager (); - JniRuntime.JniValueManager valueManager = CreateValueManager (); androidRuntime = new AndroidRuntime ( args->env, args->javaVm, args->grefLoader, - typeManager, - valueManager + new TrimmableTypeMapTypeManager (), + new TrimmableTypeMapValueManager () ); JniRuntime.SetCurrent (androidRuntime); TrimmableTypeMap.RegisterNativeMethods (); @@ -134,16 +132,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) SetSynchronizationContext (); } - internal static JniRuntime.JniTypeManager CreateTypeManager () - { - return new TrimmableTypeMapTypeManager (); - } - - internal static JniRuntime.JniValueManager CreateValueManager () - { - return new TrimmableTypeMapValueManager (); - } - static void InitializeCommonState (JnienvInitializeArgs args) { Logger.SetLogCategories ((LogCategories)args.logCategories); From e656e6ee2fa34f772aaa95546f37d45e5c782cd4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:48:25 +0200 Subject: [PATCH 24/58] Remove obsolete explicit Java type registration cache Keep the public registration API binary-compatible but report that runtime registration is unsupported by the trimmable typemap; retain attribute-based JNI class lookup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 12 +---- src/Mono.Android/Java.Interop/TypeManager.cs | 50 +------------------ .../TrimmableTypeMapTypeManagerTests.cs | 8 +++ 3 files changed, 10 insertions(+), 60 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 841e0c96f8e..1661dedf1f5 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -265,17 +265,7 @@ public static IntPtr FindClass (System.Type type) if (!((e is Java.Lang.NoClassDefFoundError) || (e is Java.Lang.ClassNotFoundException))) throw; RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"JNIEnv.FindClass(Type) caught unexpected exception: {e}"); - var jni = Java.Interop.TypeManager.GetJniTypeName (type); - if (jni != null) { - e.Dispose (); - return FindClass (JavaNativeTypeManager.ToJniName (jni, rank)); - } - - // Though it's tempting to call TypeManager.RegisterType() to avoid - // calling GetCustomAttributes() again, this isn't necessary as - // JNIEnv.FindClass() will invoke the static constructor for the type, - // which will (indirectly) call TypeManager.RegisterType(). - jni = JavaNativeTypeManager.ToJniNameFromAttributes (type); + var jni = JavaNativeTypeManager.ToJniNameFromAttributes (type); if (jni != null) { e.Dispose (); return FindClass (JavaNativeTypeManager.ToJniName (jni, rank)); diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index ce6f6950492..d500160935c 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -6,33 +6,6 @@ namespace Java.Interop { - static class TypeManagerMapDictionaries - { - static Dictionary? _jniToManaged; - static Dictionary? _managedToJni; - - public static readonly object AccessLock = new object (); - - // - // Access to both properties MUST be done after taking lock on accessLock! - // - public static Dictionary JniToManaged { - get { - if (_jniToManaged == null) - _jniToManaged = new Dictionary (StringComparer.Ordinal); - return _jniToManaged; - } - } - - public static Dictionary ManagedToJni { - get { - if (_managedToJni == null) - _managedToJni = new Dictionary (); - return _managedToJni; - } - } - } - public static partial class TypeManager { internal static string GetClassName (IntPtr class_ptr) { @@ -43,15 +16,6 @@ internal static string GetClassName (IntPtr class_ptr) return ret; } - internal static string? GetJniTypeName (Type type) - { - lock (TypeManagerMapDictionaries.AccessLock) { - if (TypeManagerMapDictionaries.ManagedToJni.TryGetValue (type, out var jni)) - return jni; - } - return null; - } - class TypeNameComparer : IComparer { public int Compare (string? x, string? y) { @@ -92,19 +56,7 @@ public int Compare (string? x, string? y) public static void RegisterType (string java_class, Type t) { - string jniFromType = JNIEnv.GetJniName (t); - lock (TypeManagerMapDictionaries.AccessLock) { - if (!TypeManagerMapDictionaries.JniToManaged.TryGetValue (java_class, out var lookup)) { - TypeManagerMapDictionaries.JniToManaged.Add (java_class, t); - if (String.Compare (jniFromType, java_class, StringComparison.OrdinalIgnoreCase) != 0) { - TypeManagerMapDictionaries.ManagedToJni.Add (t, java_class); - } - } else if (t != typeof (Java.Interop.TypeManager)) { - // skip the registration and output a warning - Logger.Log (LogLevel.Warn, "monodroid", FormattableString.Invariant ($"Type Registration Skipped for {java_class} to {t} ")); - } - - } + throw new NotSupportedException ("Explicit Java type registration is not supported with the trimmable type map. Use [Register] on Java peer types instead."); } const string TypeRegistrationNotSupported = diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index d9510d58d75..b212037982e 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -21,6 +21,14 @@ sealed class TestableTrimmableTypeMapTypeManager : TrimmableTypeMapTypeManager { } + [Test] + public void ExplicitTypeRegistrationIsNotSupported () + { + var exception = Assert.Throws (() => + Java.Interop.TypeManager.RegisterType ("example/CustomPeer", typeof (Java.Lang.Object))); + Assert.That (exception?.Message, Does.Contain ("trimmable type map")); + } + [TestCase ("android/app/Activity", "android/app/DesugarActivity$_CC", "android/app/Activity$-CC")] [TestCase ("Activity", "DesugarActivity$_CC", "Activity$-CC")] [TestCase ("com/example/package/MyInterface", "com/example/package/DesugarMyInterface$_CC", "com/example/package/MyInterface$-CC")] From 073df7fb75483e1a45830b55e350589a6d68593f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 12:11:06 +0200 Subject: [PATCH 25/58] Remove residual JVM test exclusions from runtime cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestInstrumentation.cs | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index 57e972cd18f..ff419f4d48d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -27,9 +27,6 @@ protected override IEnumerable? ExcludedCategories { categories.Add ("CoreCLRIgnore"); categories.Add ("NTLM"); - categories.Add ("NativeTypeMap"); - categories.Add ("TrimmableTypeMapUnsupported"); - // Build-time flags flow in via runtimeconfig.json properties // (see entries in Mono.Android.NET-Tests.csproj). if (HasAppContextSwitch ("PublishAot")) { @@ -37,7 +34,6 @@ protected override IEnumerable? ExcludedCategories { categories.Add ("NativeAOTIgnore"); categories.Add ("SSL"); categories.Add ("NTLM"); - } // Process-wide reference counts are only stable in the dedicated filtered run. @@ -79,24 +75,6 @@ static string [] GetIncludedCategories () static bool HasAppContextSwitch (string key) => AppContext.TryGetSwitch (key, out var value) && value; - protected override IEnumerable? ExcludedTestNames { - get { - // Tests from the external Java.Interop-Tests assembly that fail under the - // trimmable typemap. These cannot use [Category] because we don't control - // that assembly — they must be excluded by name here. - return new [] { - // Known limitation: [JniAddNativeMethodRegistrationAttribute] is not - // supported by design under the trimmable typemap. This Java.Interop-Tests - // fixture uses that attribute to register native callbacks on a hand-written - // Java peer (an obsolete code path whose primary consumer, jnimarshalmethod-gen, - // was removed in dotnet/java-interop#1405). The trimmable typemap generator - // emits XA4251 when it encounters the attribute. - // See https://github.com/dotnet/android/issues/11170. - "Java.InteropTests.InvokeVirtualFromConstructorTests", - }; - } - } - public override void OnCreate (Bundle? arguments) { Java.Lang.JavaSystem.LoadLibrary ("reuse-threads"); From 9c09248a6ec736f19106b6d8da6a8f0d77887475 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 07:58:07 +0200 Subject: [PATCH 26/58] Drop removed native app init override after rebase The updated parent no longer declares xamarin_app_init. Do not register its removed symbol in the CoreCLR p/invoke override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/native/clr/pinvoke-override/precompiled.cc | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/native/clr/pinvoke-override/precompiled.cc b/src/native/clr/pinvoke-override/precompiled.cc index 1f7042b189a..e17640bf02e 100644 --- a/src/native/clr/pinvoke-override/precompiled.cc +++ b/src/native/clr/pinvoke-override/precompiled.cc @@ -58,10 +58,6 @@ namespace { if (entrypoint_name == "clr_initialize_gc_bridge"sv) { return reinterpret_cast (&clr_initialize_gc_bridge); } - if (entrypoint_name == "xamarin_app_init"sv) { - return reinterpret_cast (&xamarin_app_init); - } - abort_missing_internal_symbol ("xa-internal-api"sv, entrypoint_name); } From e03abb7d428755947e34e13d6c83be4e350c9784 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 14:24:47 +0200 Subject: [PATCH 27/58] Remove legacy platform Java callable wrapper jar Generate framework peers only in application typemaps, and use the trimmable runtime jar for binding and app Java compilation. Do not package the obsolete platform jar or DEX in reference packs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Configuration.props | 7 -- Documentation/docs-mobile/messages/xa0002.md | 5 +- .../create-packs/Microsoft.Android.Ref.proj | 2 - .../scripts/JavaCallableWrappers.targets | 71 ------------------- src/Mono.Android/Mono.Android.csproj | 7 -- src/Mono.Android/Mono.Android.targets | 4 -- .../Android/Xamarin.Android.Javac.targets | 18 ++--- .../Tasks/Javac.cs | 7 -- .../TrimmableTypeMapBuildTests.cs | 15 ++++ .../Xamarin.Android.Common.targets | 2 +- .../TrimmableTypeMapGeneratorTests.cs | 10 +++ 11 files changed, 39 insertions(+), 109 deletions(-) delete mode 100644 build-tools/scripts/JavaCallableWrappers.targets diff --git a/Configuration.props b/Configuration.props index 28646bc673c..f1b677bb010 100644 --- a/Configuration.props +++ b/Configuration.props @@ -215,13 +215,6 @@ $(AndroidSupportedTargetJitAbis.Split(':')) - - - - true - - diff --git a/Documentation/docs-mobile/messages/xa0002.md b/Documentation/docs-mobile/messages/xa0002.md index 2e82f96a701..acc2e98425d 100644 --- a/Documentation/docs-mobile/messages/xa0002.md +++ b/Documentation/docs-mobile/messages/xa0002.md @@ -1,7 +1,7 @@ --- title: .NET for Android error XA0002 description: XA0002 error code -ms.date: 04/11/2024 +ms.date: 09/24/2026 f1_keywords: - "XA0002" --- @@ -12,7 +12,8 @@ f1_keywords: This error means the build system could not locate the *mono.android.jar* file. This probably means your installation -is corrupt in some way. +is corrupt in some way. This error applies to older SDK versions; +the .NET 11 SDK no longer includes *mono.android.jar*. ## Solution diff --git a/build-tools/create-packs/Microsoft.Android.Ref.proj b/build-tools/create-packs/Microsoft.Android.Ref.proj index 37ee75035b6..fc92623d104 100644 --- a/build-tools/create-packs/Microsoft.Android.Ref.proj +++ b/build-tools/create-packs/Microsoft.Android.Ref.proj @@ -47,8 +47,6 @@ by projects that use the Microsoft.Android framework in .NET 6+. - - diff --git a/build-tools/scripts/JavaCallableWrappers.targets b/build-tools/scripts/JavaCallableWrappers.targets deleted file mode 100644 index 4ee26c823d4..00000000000 --- a/build-tools/scripts/JavaCallableWrappers.targets +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - <_CorlibPath Include="@(ReferencePath->'%(RootDir)%(Directory)')" Condition=" '%(FileName)%(Extension)' == 'mscorlib.dll' " /> - - - <_CorlibDir>@(_CorlibPath) - $(OutputPath) - $(MSBuildProjectDirectory)/$(OutputPath) - "$(MicrosoftAndroidSdkOutDir)jcw-gen.dll" -v10 - <_LibDirs>-L "$(OutputPathAbs.TrimEnd('\'))" - <_LibDirs>$(_LibDirs) -L "$(_CorlibDir.TrimEnd('\'))" - <_Out>-o "$(MSBuildProjectDirectory)/$(IntermediateOutputPath)jcw/src" - - - - <_JavaSources Include="$(IntermediateOutputPath)jcw\src\**\*.java" /> - - - - <_Target>-source $(JavacSourceVersion) -target $(JavacTargetVersion) - <_D>-d "$(IntermediateOutputPath)jcw/bin" - <_AndroidJar>"$(AndroidSdkDirectory)\platforms\android-$(AndroidPlatformId)\android.jar" - <_MonoAndroidJar>$(OutputPath)mono.android.jar - <_MonoAndroidRuntimeJar>$(MicrosoftAndroidSdkOutDir)java_runtime.jar - - - - - - - - - - $(MicrosoftAndroidSdkOutDir)r8.jar - - - - - diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 183aaa6d818..16fc2abb69e 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -16,7 +16,6 @@ $(DefineConstants);JAVA_INTEROP $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework)\android-$(AndroidPlatformId)\ false - CoreBuild false false @@ -52,10 +51,6 @@ - - $([System.IO.Path]::GetFullPath ('$(OutputPath)$(AssemblyName).dll')) - - $(DefineConstants);ENABLE_MARSHAL_METHODS @@ -122,7 +117,6 @@ - @@ -406,7 +400,6 @@ - diff --git a/src/Mono.Android/Mono.Android.targets b/src/Mono.Android/Mono.Android.targets index 85ccc77a52f..078cb42bf23 100644 --- a/src/Mono.Android/Mono.Android.targets +++ b/src/Mono.Android/Mono.Android.targets @@ -161,10 +161,6 @@ $(DefineConstants);$([System.String]::Copy('$(_GeneratedDefineConstants)').Replace ('%24(DefineConstants);', '')) - - - - - + + DependsOnTargets="_CollectRuntimeJarFilenames;_CategorizeAndroidLibraries;_CollectJavaSourceForBinding;_CompileBindingJava"> @@ -150,8 +152,8 @@ It is shared between "legacy" binding projects and .NET 7+ projects. diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Javac.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Javac.cs index f01c6edb6d1..b2966d72095 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Javac.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Javac.cs @@ -50,13 +50,6 @@ public override bool RunTask () protected override string GenerateCommandLineCommands () { - // Running command: C:\Program Files (x86)\Java\jdk1.6.0_20\bin\javac.exe - // "-J-Dfile.encoding=UTF8" - // "-d" "bin\classes" - // "-classpath" "C:\Users\Jonathan\Documents\Visual Studio 2010\Projects\AndroidMSBuildTest\AndroidMSBuildTest\obj\Debug\android\bin\mono.android.jar";"C:\Program Files (x86)\Android\android-sdk-windows\platforms\android-8\android.jar" - // "-encoding" "UTF-8" - // "@C:\Users\Jonathan\AppData\Local\Temp\tmp79c4ac38.tmp" - var cmd = new CommandLineBuilder (); cmd.AppendSwitchIfNotNull ("-J-Dfile.encoding=", "UTF8"); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index d610504fdbd..f7467a983e1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -36,6 +36,13 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); AssertTrimmableTypeMapOutputs (intermediateDir); + if (!isRelease) { + var frameworkJcw = Path.Combine (intermediateDir, "java", "android", "app", "ActivityTracker.java"); + FileAssert.Exists (frameworkJcw); + var source = File.ReadAllText (frameworkJcw); + StringAssert.Contains ("mono.android.Runtime.registerNatives (ActivityTracker.class)", source); + StringAssert.DoesNotContain ("mono.android.TypeManager.Activate", source); + } } [TestCase (AndroidRuntime.CoreCLR)] @@ -2220,6 +2227,14 @@ public void TrimmableTypeMap_PreserveLists_ArePackagedInSdk () } } + [Test] + public void TrimmableTypeMap_ReferencePack_DoesNotShipLegacyPlatformJcws () + { + var referenceDirectory = TestEnvironment.MonoAndroidFrameworkDirectory; + FileAssert.DoesNotExist (Path.Combine (referenceDirectory, "mono.android.jar")); + FileAssert.DoesNotExist (Path.Combine (referenceDirectory, "mono.android.dex")); + } + [Test] public void TrimmableTypeMap_RuntimeArtifacts_ArePackagedInSdk () { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 543fd08ed66..5a54c3d1c1e 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1354,7 +1354,7 @@ because xbuild doesn't support framework reference assemblies. s.RelativePath == "xamarin/android/net/ServerCertificateCustomValidator_TrustManager_FakeSSLSession.java"); Assert.Contains (result.GeneratedJavaSources, s => s.RelativePath == "xamarin/android/net/ServerCertificateCustomValidator_AlwaysAcceptingHostnameVerifier.java"); Assert.Contains (result.GeneratedJavaSources, s => s.RelativePath == "xamarin/android/net/ServerCertificateCustomValidator_NonRequiredFrameworkAcw.java"); + var frameworkJcw = Assert.Single (result.GeneratedJavaSources, + s => s.RelativePath == "xamarin/android/net/ServerCertificateCustomValidator_TrustManager.java"); + Assert.Contains ("mono.android.Runtime.registerNatives (ServerCertificateCustomValidator_TrustManager.class)", frameworkJcw.Content); + Assert.DoesNotContain ("mono.android.TypeManager.Activate", frameworkJcw.Content); + var frameworkConstructorJcw = Assert.Single (result.GeneratedJavaSources, + s => s.RelativePath == "myapp/PlainActivitySubclass.java"); + Assert.Contains ("mono.android.Runtime.registerNatives (PlainActivitySubclass.class)", frameworkConstructorJcw.Content); + Assert.Contains ("private native void nctor_0 ()", frameworkConstructorJcw.Content); + Assert.DoesNotContain ("mono.android.TypeManager.Activate", frameworkConstructorJcw.Content); + Assert.Contains (result.GeneratedAssemblies, assembly => assembly.Name == "_Mono.Android.TypeMap"); } [Fact] From 64c1ea508e5cbd57f9e419b16647646e09f6331c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:08:40 +0200 Subject: [PATCH 28/58] Document trimmable JNI interoperability Describe generated Java wrappers, managed TypeMaps, JNI registration, peer lifetime, and CoreCLR/NativeAOT interop. Link the guide from the documentation index. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/README.md | 1 + .../guides/internals/JavaJNI_Interop.md | 923 +++--------------- 2 files changed, 163 insertions(+), 761 deletions(-) diff --git a/Documentation/README.md b/Documentation/README.md index 826dff3e082..14e2a93b9fa 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -23,6 +23,7 @@ * [`.axml` CodeBehind Support](docs-mobile/features/layout-code-behind/index.md) * [MSBuild Best Practices](guides/MSBuildBestPractices.md) * [D8 and R8 Integration](guides/D8andR8.md) + * [Java and managed interoperability](guides/internals/JavaJNI_Interop.md) * [Profiling Startup](guides/profiling.md) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 2d6e8c82d72..7d06fa1f544 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,766 +1,167 @@ - -- [Introduction](#introduction) -- [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) - - [Java Callable Wrappers (JCW)](#java-callable-wrappers-jcw) -- [Registration](#registration) - - [Dynamic registration](#dynamic-registration) - - [Dynamic Java Callable Wrappers registration code](#dynamic-java-callable-wrappers-registration-code) - - [Dynamic Registration call sequence](#dynamic-registration-call-sequence) - - [Marshal methods](#marshal-methods) - - [Marshal Methods Java Callable Wrappers registration code](#marshal-methods-java-callable-wrappers-registration-code) - - [Marshal methods C# source code](#marshal-methods-c-source-code) - - [JNI requirements](#jni-requirements) - - [LLVM IR code generation](#llvm-ir-code-generation) - - [Assembly rewriting](#assembly-rewriting) - - [Wrappers for methods with non-blittable types](#wrappers-for-methods-with-non-blittable-types) - - [UnmanagedCallersOnly attribute](#unmanagedcallersonly-attribute) - - [Marshal Methods Registration call sequence](#marshal-methods-registration-call-sequence) - - -# Introduction - -At the core of `.NET for Android` is its ability to interoperate with -the Java/Kotlin APIs implemented in the Android system. To make it -work, it is necessary to "bridge" the two separate worlds of Java VM -(`ART` in the Android OS) and the Managed VM (`MonoVM`). Application -developers expect to be able to call native Android APIs and receive -calls (or react to events) from the Android side using code written in -one of the .NET managed languages. To make it work, `.NET for Android` -employs a number of techniques, both at build and at run time, which -are described in the sections below. - -This guide is meant to explain the technical implementation in a way -that is sufficient to understand the system without having to read the -actual source code. - -# Java <-> Managed interoperability overview - -Java VM and Managed VM are two entirely separate entities which -co-exist in the same process/application. Despite sharing the same -process resources, they don't "naturally" communicate with each other. -There is no direct way to call Java/Kotlin from .NET a'la the -`p/invoke` mechanism which allows calling native code APIs. Nor there -exists a way for Java/Kotlin code to invoke managed methods. To make -it possible, `.NET for Android` takes advantage of the Java's [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html) -(`Java Native Interface`), a mechanism that allows native code -(.NET managed code being "native" in this context) to register -implementations of Java methods, written outside the Java VM and in -languages other than Java/Kotlin (for instance in `C`, `C++` or -`Rust`). - -Such methods need to be appropriately declared in the Java code, for -instance: - -```java -class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Each native method is declared using the `native` keyword, and -whenever it is invoked from other Java code, the Java VM will use the -JNI to invoke the target method. - -Native methods can be registered either dynamically (by calling the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function) or "statically", by providing a native shared library -which exports a symbol with appropriate name which points to the -native function implementing the Java method. - -Both ways of registration are described in detail in the following -sections. - -## Java Callable Wrappers (JCW) - -`.NET for Android` wraps the entire Android API by generating -appropriate C# code which mirrors the Java/Kotlin code (classes, -interfaces, methods, properties etc). Each generated class that -corresponds to a Java/Kotlin type, is derived from the -`Java.Lang.Object` class (implemented in the `Mono.Android` assembly), -which marks it as a "Java interoperable type", meaning that it can -implement or override virtual Java methods. To make registration and -invoking of such methods possible, it is necessary to generate a Java -class which mirrors the Managed one and provides an entry point to -the Java <-> Managed transition. The Java classes are generated -during application (as well as `.NET for Android`) build and we call -them **Java Callable Wrappers** (or **JCW** for short). For instance, -the following managed class: - -```csharp -public class MainActivity : AppCompatActivity -{ - public override Android.Views.View? OnCreateView (Android.Views.View? parent, string name, Android.Content.Context context, Android.Util.IAttributeSet attrs) - { - return base.OnCreateView (parent, name, context, attrs); - } - - protected override void OnCreate (Bundle savedInstanceState) - { - base.OnCreate(savedInstanceState); - DoSomething (savedInstanceState); - } - - void DoSomething (Bundle bundle) - { - // do something with the bundle - } -} -``` - -overrides two Java virtual methods found in the `AppCompatActivity` -type: `OnCreateView` and `OnCreate`. The `DoSomething` method does -not correspond to any method found in the base Java type, and thus it -won't be included in the JCW. - -The Java Callable Wrapper generated for the above class would look as -follows (a few generated methods not relevant to the discussion have -been omitted for brevity): +# Java and managed interoperability (trimmable TypeMap) + +This guide describes the **current** .NET for Android application interop +path for CoreCLR and NativeAOT. `AndroidTypeMapImplementation=trimmable` is the +only supported application TypeMap implementation. The Java VM (ART) and the +managed runtime communicate through JNI; the build supplies generated Java +callable wrappers (JCWs) and managed TypeMap assemblies, while handwritten +Java and managed runtime code connects those generated pieces. This is not a +description of the former MonoVM/native LLVM TypeMap or assembly-rewriting +pipeline. + +## What is generated + +| Piece | Purpose and source | +| --- | --- | +| Bound managed API types | Managed callable wrappers for Android/Java APIs use `JniPeerMembers` to invoke Java methods. These are distinct from the app's Java callable wrappers; see [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) for an invocation. | +| JCW `.java` files | Java classes for managed Java peers, with constructors and `n_*` native callbacks for Java-to-managed calls. [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) produces them from the scanned peer model. | +| TypeMap DLLs | `_{AssemblyName}.TypeMap.dll` files hold mappings, proxy types, and native registration methods; `_Microsoft.Android.TypeMaps.dll` is the root that connects them. [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and [`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs) emit these managed assemblies. | +| Registration and manifest | The task generates `ApplicationRegistration.java` from the scanned peers (not from a Java template); it also writes the ACW map (`acw-map.txt`) and merged `AndroidManifest.xml`. | + +The [scanner](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +reads assembly metadata to discover Java peers and their Java-visible +constructors and methods. The +[`GenerateTrimmableTypeMap` task](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) +drives the scan, validates it, emits the managed and Java outputs, and removes +stale JCW sources. The +[shared MSBuild targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) +run `_GenerateTrimmableTypeMap` after `CoreCompile`; their output lives under +`obj/.../typemap/` (not in handwritten source). Inspect +`typemap/typemap-assemblies.txt` and `typemap/java-files.txt` to see what +that build actually generated. The +[TypeMap build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) +documents target ordering, output stamps, and incremental regeneration. + +The root TypeMap assembly anchors the per-assembly TypeMaps and initializes +the lookup dictionaries. Mapping attributes in those per-assembly DLLs +associate Java names, managed types, and generated proxy types. ACW proxies +implement `IAndroidCallableWrapper` for native registration; other proxies +do not need that interface. These are **managed metadata**, not a lookup +table in a native binary. The runtime +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses the mappings to resolve Java objects and managed peer types. + +## Managed calls Java + +A bound C# method uses its `JniPeerMembers` and a JNI member signature to +call Java through Java.Interop. For example, +`Android.App.Activity.ReportFullyDrawn()` invokes +`_members.InstanceMethods.InvokeVirtualVoidMethod(...)` with +`"reportFullyDrawn.()V"` and the current peer. Static calls use the +corresponding `StaticMethods` APIs. Arguments and return values cross JNI +with the appropriate Java object references and value conversions. This +direction does **not** call a JCW's `n_*` callback: the bound managed API +is the caller, and the Java implementation is the callee. + +When a JNI object arrives in managed code, +[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +resolves its Java type to a managed target/proxy using the generated map; +[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +reuses or constructs a managed peer for its JNI reference. Conversely, the +type manager resolves managed Java-peer types to JNI type signatures when +needed. The map is about **type and peer resolution**; it does not replace +normal Java.Interop method invocation. + +## Java calls managed code + +The JCW is a real Java class generated for a managed peer. Its Java-visible +methods forward to `native n_*` methods, and its constructors use generated +`nctor_*` callbacks. The generator normally emits a per-class static +initializer like: ```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Understanding the connection between Managed methods and their Java -counterparts is required in order to understand the registration -mechanisms described in sections found later in this document. The -[Dynamic registration](#dynamic-registration) section will expand on -this example in order to explain the details of how the Managed type -and its methods are registered with the Java VM. - -# Registration - -Both mechanisms of method registration rely on generation of [Java -Callable Wrappers](#java-callable-wrappers-jcw), with [Dynamic -registration](#dynamic-registration) requiring more code to be -generated so that the registration can be performed at the runtime. - -JCW are generated only for types that derive from -the `Java.Lang.Object` type. Finding such types is the task of the -Java.Interop's [`JavaTypeScanner`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaTypeScanner.cs), -which uses `Mono.Cecil` to read all the assemblies referenced by the -application and its libraries. The returned list of assemblies is -then used by a variety of tasks, JCW being only one -of them. - -After all types are found, -[`JavaCallableWrapperGenerator`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -is invoked in order to analyze each method in each type, looking for -those which override a virtual Java method and, thus, need to be -included in the wrapper class code. The generator optionally (if -[marshal methods](#marshal-methods) are enabled) passes each method to -an implementation of the -[`Java.Interop.Tools.JavaCallableWrappers.JavaCallableMethodClassifier`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -abstract class (which is -[`MarshalMethodsClassifier`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs) -in our case), to check whether the given method can be registered -statically. - -`JavaCallableWrapperGenerator` looks for methods decorated with the -`[Register]` attribute, which most frequently is created by invoking -its constructor with three parameters: - - 1. Java method name - 2. JNI method signature - 3. "Connector" method name - -The "connector" is a static method which creates a delegate that -subsequently allows calling of the native callback method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -The above code is actually generated in the `Android.App.Activity` -class while .NET for Android is built, from which our example -`MainActivity` eventually derives. - -What happens with the above code depends on the registration mechanism -and is described in the sections below. - -## Dynamic registration - -This registration mechanism has been used by `.NET for Android` since -the beginning and it will remain in use for the foreseeable future -when the application is built in the `Debug` configuration or when -[Marshal Methods](#marshal-methods) are turned off. - -### Dynamic Java Callable Wrappers registration code - -Building on the C# example shown in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ -/** @hide */ - public static final String __md_methods; - static { - __md_methods = - "n_onCreateView:(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;:GetOnCreateView_Landroid_view_View_Ljava_lang_String_Landroid_content_Context_Landroid_util_AttributeSet_Handler\n" + - "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" + - ""; - mono.android.Runtime.register ("HelloAndroid.MainActivity, HelloAndroid", MainActivity.class, __md_methods); - } - - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Code fragment which takes part in registration is the class's static -constructor. For each method registered for the type (that is, -implemented or overridden in the managed code), the JCW generator -outputs a single string which contains full information about the type -and method to register. Each such registration string is terminated -with the newline character and the entire sequence ends with an empty -string. Together, all the lines are concatenated and placed in the -`__md_methods` static variable. The `mono.android.Runtime.register` -method (see below for more details) is then invoked to register all -the methods. - -### Dynamic Registration call sequence - -All the "native" methods declared in the generated Java type are -registered when the type is constructed or accessed for the first -time. This is when the Java VM invokes the type's static constructor, -kicking off a sequence of calls that eventually ends with all the type -methods registered with JNI: - - 1. `mono.android.Runtime.register` is itself a native method, - declared in the - [`Runtime`](../../src/java-runtime/java/mono/android/Runtime.java) - class of .NET for Android's Java runtime code, and implemented in - the native .NET for Android - [runtime](../../src/monodroid/jni/monodroid-glue.cc) (the - `MonodroidRuntime::Java_mono_android_Runtime_register` method). - Purpose of this method is to prepare a call into the - .NET for Android managed runtime code, the - [`Android.Runtime.JNIEnv::RegisterJniNatives`](../../src/Mono.Android/Android.Runtime/JNIEnv.cs) - method. - 2. `Android.Runtime.JNIEnv::RegisterJniNatives` is passed name of - the managed type for which to register Java methods and uses .NET - reflection to load that type, followed by a call to cache the - type (via `RegisterType` method in the - [`TypeManager`](../../src/Mono.Android/Java.Interop/TypeManager.cs) - class) to end with a call to the - `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - method. - 3. `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - eventually calls the - `Java.Interop.JniEnvironment.Types::RegisterNatives` method which - first generates a delegate to the native callback method, using - `System.Reflection.Emit` (via the - [`Android.Runtime.JNINativeWrapper::CreateDelegate`](../../src/Mono.Android/Android.Runtime/JNINativeWrapper.cs) - method) and, eventually, invokes Java JNI's `RegisterNatives` - function, finally registering the native methods for a managed - type. - -The `System.Reflection.Emit` sequence mentioned in 3. above is among -the most costly operations, repeated for each registered method. - -Some more information about Java type registration can be found -[here](https://github.com/dotnet/android/wiki/Blueprint#java-type-registration). - -## Marshal methods - -The goal of marshal methods is to completely bypass the [dynamic -registration sequence](#dynamic-registration-call-sequence), replacing -it with native code generated and compiled during application build, -thus saving on the startup time of the application. - -Marshal methods registration mechanism takes advantage of the JNI -ability to look up implementations of `native` Java methods in actual -native (shared) libraries. Such symbols must have names that follow a -set of rules, so that JNI is able to properly locate them (details are -explained in the [JNI Requirements](#jni-requirements) section below). - -To achieve that, the marshal methods mechanism uses a number of -classes which [generate native](#llvm-ir-code-generation) code and -[modify assemblies](#assembly-rewriting) that contain the registered -methods. - -Current implementation of the marshal methods classifier recognizes -the "standard" method registration pattern, using the example of the -`OnCreate` method shown in [Registration](#registration) above. - -The standard pattern consists of: - - * the "connector" method, `GetOnCreate_Landroid_os_Bundle_Handler` - above - * the delegate backing field, `cb_onCreate_Landroid_os_Bundle_` - above - * the native callback method, `n_OnCreate_Landroid_os_Bundle_` above - * and the virtual target method which dispatches the call to the - actual object, `OnCreate` above. - -Whenever the classifier's `ShouldBeDynamicallyRegistered` method is -called, it is passed not only the method's declaring type, but also -the `Register` attribute instance which it then uses to check whether -the method being registered conforms to the "standard" registration -pattern shown above. The connector, native callback methods as well -as the backing field must be private and static in order for the -registered method to be considered as a candidate for static -registration. - -Registered methods which don't follow the "standard" pattern will be -registered dynamically. - -### Marshal Methods Java Callable Wrappers registration code - -Building on the C# example show in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); +static { + mono.android.Runtime.registerNatives (MyActivity.class); } ``` -Note that, compared to the code generated for the [dynamic -registration](#dynamic-java-callable-wrappers-registration-code) -mechanism, there is no static constructor while the rest -of the code remains exactly the same. - -### Marshal methods C# source code - -The marshal methods sections below will all refer to this code -fragment: - - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -### JNI requirements - -JNI -[specifies](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names) -a number of rules which govern how the native symbol name is -constructed, so that a mapping of object-oriented Java code (with its -package names/namespaces, class names and overloadable methods) into the essentially -"flat" procedural "namespace" of the lowest common denominator C code. - -The precise rules are outlined in the URL above, and their short -version is as follows: - - * Each symbol starts with the `Java_` prefix - * Next follows the mangled (see below) **fully qualified class - name** - * Next the `_` character serves as a separator before - * A mangled **method** name, which is optionally followed by - * Double underscore `__` and the mangled method argument signature - -"Mangling" is a way of encoding certain characters that are not -directly representable both in the source code and in the native -symbol name. The JNI specification allows for direct use of ASCII -letters (capital and lowercase) and digits, while all the other -characters are either represented by placeholders or encoded as 16-bit -hexadecimal Unicode character code (table copied from the JNI -specification for easier reference): - -| Escape sequence | Denotes | -|-----------------|------------------------------------------| -| _0XXXX | a Unicode character XXXX, all lower case | -| _1 | The `_` character | -| _2 | The `;` character in signatures | -| _3 | The `[` character in signatures | -| _ | The `.` or `/` characters | - -Generation of JNI symbol names is performed by the -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -class while generating the native function source code. - -JNI supports two forms of the native symbol name, as signalled in the -bullet list above - a short and a long one. The former is looked up -first by the Java VM, followed by the latter. The latter needs to be -used only for overloaded methods, which is what our generator does. - -### LLVM IR code generation - -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -uses the LLVM IR generator infrastructure to output both data and -executable code for all the marshal methods wrappers. It is not -necessary to understand the generated code unless one needs to modify -it, so this document only shows the equivalent C++ code which can -serve as a guide to understanding how the marshal method runtime -invocation works: - -```C++ -using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr); - -static get_function_pointer_fn get_function_pointer; - -void xamarin_app_init (get_function_pointer_fn fn) noexcept -{ - get_function_pointer = fn; -} - -using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState); -static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr; - -extern "C" JNIEXPORT void -JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept -{ - if (android_app_activity_on_create_bundle == nullptr) { - get_function_pointer ( - 16, // mono image index - 0, // class index - 0x0600055B, // method token - reinterpret_cast(android_app_activity_on_create_bundle) // target pointer - ); - } - - android_app_activity_on_create_bundle (env, klass, savedInstanceState); -} -``` - -The `xamarin_app_init` function is output only once and is called by -the `.NET for Android` runtime twice during application startup - once -to pass `get_function_pointer_fn` which does **not** use any locks (as -we know that until a certain point during startup we are in a single -lock, so no data access races can happen) and the other time just -before handing control over to the MonoVM, to pass pointer to -`get_function_pointer_fn` which **does** employ locking (since during -runtime it may very well happen that our generated Java native -functions will be called from different threads simultaneously). - -The `Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2` -function is a template which is repeated for each Java native -function, with each function having its own set of arguments and its -own callback backing field (`android_app_activity_on_create_bundle` -here). - -The `get_function_pointer` function takes as parameters indexes into a -couple of tables, one for `MonoImage*` pointers and the other for -`MonoClass*` pointers - both of which are generated by the -`MarshalMethodsNativeAssemblyGenerator` class at application build -time and allow for very fast lookup during run time. Target methods -are retrieved by their token value, within the specified `MonoImage*` -(essentially a pointer to managed assembly image in memory) and class. - -The method identified in such manner, **must** be decorated in the -managed code with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute (see [below](#unmanagedcallersonly-attribute) for more -details) so that it can be invoked directly, as if it was a native -method itself, with minimal managed marshaling overhead. - -### Assembly rewriting - -Please refer to the [C# code fragment](#marshal-methods-c-source-code) -above in order to understand this section. - -Managed assemblies (including `Mono.Android.dll`) which contain Java -types need to be usable in two contexts: with the "traditional" -dynamic registration and with marshal methods. Both of these -mechanisms, however, have different requirements. We cannot assume -that any assembly (either from .NET for Android or a third party nuget) -will have "marshal methods friendly" code and thus we need to make -sure that the code meets our requirements. - -We do it by reading each relevant assembly and modifying it by -altering the definition of the native callbacks and removing the -code that's no longer used by marshal methods. This task is performed -by the -[`MarshalMethodsAssemblyRewriter`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs) -invoked during application build after all the assemblies are linked -but **before** type maps are generated (as rewriting **will** alter -the method and potentially type tokens) - -The exact modifications we apply are: - - * Removal of the **connector backing field** - * Removal of the **connector method** - * Generation of a **native callback wrapper** method, which catches - and propagates unhandled exceptions thrown by the native callback - or the target method. This method is decorated with the - `[UnmanagedCallersOnly]` attribute and called directly from the - native code. - * Optionally, generate code in the **native callback wrapper** to handle - [non-blittable types](#wrappers-for-methods-with-non-blittable-types). - -All the modifications are performed with `Mono.Cecil`. - -After modifications, the assembly contains equivalent of the following -C# code for each marshal method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Native callback exception wrapper - [UnmanagedCallersOnly] - static void n_OnCreate_Landroid_os_Bundle__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - try { - n_OnCreate_Landroid_os_Bundle_ (jnienv, native__this, native_savedInstanceState) - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - } - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -#### Wrappers for methods with non-blittable types - -The -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute requires that all the argument types as well as the method -return type are -[blittable](https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types). - -Among these types is one that's commonly used by the managed classes -implementing Java methods: `bool`. Currently this is the **only** -non-blittable type we've encountered in bindings, so at this point it -is the only one supported by the assembly rewriter. - -Whenever we encounter a method with a non-blittable type, we must -generate a wrapper for it, so that we can decorate it with the -`[UnmanagedCallersOnly]` attribute. This is easier and less error -prone than modifying the native callback method's IL stream to -implement the necessary conversion. - -An example of such method is -`Android.Views.View.IOnTouchListener::OnTouch`: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} -``` - -As it returns a `bool` value, it needs a wrapper to cast the return -value properly. Each wrapper method retains the native callback method -name, but appends the `_mm_wrapper` suffix to it: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} - -[UnmanagedCallersOnly] -static byte n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - try { - return n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(jnienv, native__this, native_v, native_e) ? 1 : 0; - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - return default; - } -} -``` - -The wrapper's return statement uses the ternary operator to "cast" the -boolean value to `1` (for `true`) or `0` (for `false`) because the -value of `bool` across the managed runtime can take a range of values: - - * `0` for `false` - * `-1` or `1` for `true` - * `!= 0` for true - -Since the `bool` type in C# can be 1, 2 or 4 bytes long, we need to -cast it to some type of a known and static size. The managed type -`byte` was chosen as it corresponds to the Java/JNI `jboolean` type, -defined as an unsigned 8-bit type. - -Whenever an **argument** value needs to be converted between `byte` and -`bool`, we generate code that is equivalent of the `argument != 0` -comparison, for instance for the -`Android.Views.View.IOnFocusChangeListener::OnFocusChange` method: - -```csharp -[UnmanagedCallersOnly] -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, byte hasFocus) -{ - n_OnFocusChange_Landroid_view_View_Z (jnienv, native__this, native_v, hasFocus != 0); -} - -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, bool hasFocus) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - __this.OnFocusChange (v, hasFocus); -} -``` - -#### UnmanagedCallersOnly attribute - -Each marshal methods native callback method is decorated with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute, in order for us to be able to invoke the callback directly -from native code with minimal overhead compared to traditional -managed-from-native method calls (`mono_runtime_invoke`) - -### Marshal Methods Registration call sequence - -The sequence described in the [dynamic -registration sequence](#dynamic-registration-call-sequence) section -above is completely removed for the marshal methods. What remains -common for both dynamic and marshal methods registration, is the -resolution of the native function target done by the Java VM runtime. -In both cases the method declared in a Java class as `native` is -looked up by the Java VM when first JIT-ing the code. The difference -lies in the way this lookup is performed. - -Dynamic registration uses the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function at the runtime, which stores a pointer to the registered -method inside the structure which describes a Java class in the Java -VM. - -Marshal methods, however, don't register anything with the JNI, -instead they rely on the symbol lookup mechanism of the Java VM. -Whenever a call to `native` Java method is JIT-ed and it is not -registered previously using the `RegisterNatives` JNI function, Java -VM will proceed to look for symbols in the process runtime image (e.g. -using `dlopen` + `dlsym` calls on Unix) and, having found a matching -symbol, use pointer to it as the target of the `native` Java method -call. +This is a representative *generated* shape, not a source file to edit. +Application and instrumentation wrappers defer registration through a +generated guarded `__md_registerNatives()` method where static initialization +would be too early. The `registerNatives` declaration itself is **handwritten** +in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java). + +At startup, +[`JNIEnvInit`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs) +installs the trimmable type and value managers and calls +`TrimmableTypeMap.RegisterNativeMethods()`. That binds +`mono/android/Runtime.registerNatives` to a managed callback using JNI +`RegisterNatives`. When a generated JCW requests registration for its +class, `TrimmableTypeMap.OnRegisterNatives` looks up that Java class in the +generated map and asks its generated `IAndroidCallableWrapper` registration +code to bind the class's `n_*` methods. ART can then call those registered +methods, which transition through generated managed JNI-ABI entrypoints to +the managed implementation. + +The [generator model](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) +records method name, JNI signature, and callback target for each native +registration. For exported or directly callable managed methods, +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) +emits an `UnmanagedCallersOnly` entrypoint that converts JNI arguments, calls +the managed method, converts its result, and handles exceptions. Other +callbacks use the generated registration's corresponding marshal target. +Do not confuse these **generated managed** entrypoints with the handwritten +Java declarations or the old native registration table. + +## Startup and peer lifetime + +CoreCLR's [native host](../../../src/native/clr/host/host.cc) captures the +Java VM at `JNI_OnLoad`, starts the managed runtime from +`Runtime.initInternal`, and enters `JNIEnvInit.Initialize`. The native +`Runtime.register` entrypoint remains a compatibility stub; TypeMap-based +native registration is handled on the managed side. + +NativeAOT instead enters +[`JavaInteropRuntime.JNI_OnLoad` / `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs), +constructs a +[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), +and calls `JNIEnvInit.InitializeNativeAotRuntime`. Both runtime paths +register the same `TrimmableTypeMap` JNI callback, but NativeAOT cannot +depend on CoreCLR's reflection-based fallback for missing mappings. +[`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) +provides the generated proxy's AOT-safe peer creation hook. + +A managed `Java.Lang.Object` and its Java object are peers, not one object +shared by two garbage collectors. Generated JCWs implement the Java-side +[`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) +reference hooks; the runtime's +[`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) +and `TrimmableTypeMapValueManager` register, find, replace, and finalize +managed peers while respecting JNI reference ownership. Disposing a managed +peer or letting collection retire a pair can remove the association; keeping +a raw JNI handle after that does not keep the old managed instance alive. + +## Trimming, AOT, and diagnosing failures + +The [CoreCLR targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) +feed the generated TypeMap assemblies to ILLink as trimmable inputs. With +`PublishTrimmed=true`, they regenerate JCWs into `typemap/linked-java/` from +the **linked** assemblies, so compiled Java matches the surviving managed +callbacks. Check `typemap/linked-java-files.txt` rather than only the +pre-trim `java-files.txt` in such a build. The +[NativeAOT targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) +feed TypeMaps to ILC and use its output plus the ACW map for Java shrinker +configuration. Both pipelines depend on reachable managed types and their +generated registrations; a method or type missing after trimming is not +fixed by restoring a historical native TypeMap path. + +When investigating: + +1. Check the build's generated TypeMap DLL list, JCW list, `acw-map.txt`, + and compiled Java sources (post-trim `linked-java/` if applicable). + Compare the actual Java class/name/signature with the generated native + registration and its managed target. +2. Check `adb logcat` for a Java `UnsatisfiedLinkError`, JNI registration + failure, or managed exception during activation; distinguish a missing + callback from a missing managed peer mapping. Inspect the runtime's + `TrimmableTypeMap` and generated proxy paths for the latter. +3. For invalid or prematurely disposed JNI object references, use + [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). For build + errors about unsupported `AndroidTypeMapImplementation` values, see + [the build property](../../docs-mobile/building-apps/build-properties.md); + duplicate Java-visible constructor signatures are reported as + [XA4259](../../docs-mobile/messages/xa4259.md). + +The [trimmable TypeMap build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) +cover generation, trimming, NativeAOT and CoreCLR packaging, and incremental +Java output. Start there when changing generated interop behavior. From 68a715bf9ca159f72996045951a8e53d7fcfdd93 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:25:04 +0200 Subject: [PATCH 29/58] Expand trimmable JNI interop guide with generated code paths Explain the former registration concepts using current TypeMap and JCW examples, managed UTF-8 JNI registration, ABI marshalling, startup, peer lifecycle, and trimming. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../guides/internals/JavaJNI_Interop.md | 607 +++++++++++++----- 1 file changed, 461 insertions(+), 146 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 7d06fa1f544..542a9c09078 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,167 +1,482 @@ -# Java and managed interoperability (trimmable TypeMap) +# Java and managed interoperability with the trimmable TypeMap -This guide describes the **current** .NET for Android application interop -path for CoreCLR and NativeAOT. `AndroidTypeMapImplementation=trimmable` is the -only supported application TypeMap implementation. The Java VM (ART) and the -managed runtime communicate through JNI; the build supplies generated Java -callable wrappers (JCWs) and managed TypeMap assemblies, while handwritten -Java and managed runtime code connects those generated pieces. This is not a -description of the former MonoVM/native LLVM TypeMap or assembly-rewriting -pipeline. +This is an internal guide to the interop path used by **CoreCLR and NativeAOT +Android applications**. The Java VM (ART) and the managed runtime are separate +environments in one process. JNI is the boundary in both directions: bound C# +types call Java methods through Java.Interop, and Java calls managed methods +through generated Java callable wrappers (JCWs) with registered JNI entrypoints. -## What is generated +`AndroidTypeMapImplementation=trimmable` is the only supported application +TypeMap implementation. Earlier versions of this guide described a choice +between reflection/delegate-based "dynamic" registration and native +LLVM-generated marshal methods. Neither describes this path. Here, the build +generates **managed** TypeMap assemblies (including proxy and marshal code) +and Java sources; the managed runtime registers their native callbacks with +ART. There is no application-level native TypeMap lookup table, generated +`Java_...` symbol for each callback, or marshal-method callback rewriting. -| Piece | Purpose and source | +| Earlier guide | Trimmable TypeMap path | | --- | --- | -| Bound managed API types | Managed callable wrappers for Android/Java APIs use `JniPeerMembers` to invoke Java methods. These are distinct from the app's Java callable wrappers; see [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) for an invocation. | -| JCW `.java` files | Java classes for managed Java peers, with constructors and `n_*` native callbacks for Java-to-managed calls. [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) produces them from the scanned peer model. | -| TypeMap DLLs | `_{AssemblyName}.TypeMap.dll` files hold mappings, proxy types, and native registration methods; `_Microsoft.Android.TypeMaps.dll` is the root that connects them. [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and [`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs) emit these managed assemblies. | -| Registration and manifest | The task generates `ApplicationRegistration.java` from the scanned peers (not from a Java template); it also writes the ACW map (`acw-map.txt`) and merged `AndroidManifest.xml`. | - -The [scanner](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -reads assembly metadata to discover Java peers and their Java-visible -constructors and methods. The -[`GenerateTrimmableTypeMap` task](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) -drives the scan, validates it, emits the managed and Java outputs, and removes -stale JCW sources. The -[shared MSBuild targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) -run `_GenerateTrimmableTypeMap` after `CoreCompile`; their output lives under -`obj/.../typemap/` (not in handwritten source). Inspect -`typemap/typemap-assemblies.txt` and `typemap/java-files.txt` to see what -that build actually generated. The -[TypeMap build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) -documents target ordering, output stamps, and incremental regeneration. - -The root TypeMap assembly anchors the per-assembly TypeMaps and initializes -the lookup dictionaries. Mapping attributes in those per-assembly DLLs -associate Java names, managed types, and generated proxy types. ACW proxies -implement `IAndroidCallableWrapper` for native registration; other proxies -do not need that interface. These are **managed metadata**, not a lookup -table in a native binary. The runtime -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses the mappings to resolve Java objects and managed peer types. +| JCW static initializer built `__md_methods` and called `Runtime.register` with an assembly-qualified type name. | A generated JCW calls `Runtime.registerNatives(Class)`; the generated managed proxy supplies the registrations for that Java class. | +| "Dynamic" registration used connector delegates and reflection/`Reflection.Emit` at runtime. | The scanner reads connector metadata to locate existing callbacks, but generated `UnmanagedCallersOnly` forwarders register their function pointers without making per-method delegates. | +| "Marshal methods" relied on JNI `Java_...` native exports, native LLVM code, and a callback assembly rewriter. | The TypeMap emitter writes managed proxy/entrypoint IL; `RegisterNatives` binds method name/signature pairs to those entrypoints. | +| JNI symbol mangling selected the native implementation; `_mm_wrapper` adapted booleans and exceptions. | The generated registration table uses NUL-terminated UTF-8 names/signatures; generated managed entrypoints perform JNI ABI conversion and exception handoff. | -## Managed calls Java +## Terms and a running example -A bound C# method uses its `JniPeerMembers` and a JNI member signature to -call Java through Java.Interop. For example, -`Android.App.Activity.ReportFullyDrawn()` invokes -`_members.InstanceMethods.InvokeVirtualVoidMethod(...)` with -`"reportFullyDrawn.()V"` and the current peer. Static calls use the -corresponding `StaticMethods` APIs. Arguments and return values cross JNI -with the appropriate Java object references and value conversions. This -direction does **not** call a JCW's `n_*` callback: the bound managed API -is the caller, and the Java implementation is the callee. +| Term | Meaning | +| --- | --- | +| Bound managed type (MCW) | A C# binding for an existing Java class, such as `Android.App.Activity`. Its `JniPeerMembers` invokes Java methods. | +| Java callable wrapper (JCW, or ACW) | A *generated Java class* for a managed subclass or Java-visible implementation. Java overrides call `native n_*` methods, which lead back to managed code. | +| TypeMap assembly | A *generated managed DLL* containing Java-name/managed-type associations, proxy types, and (for JCWs) registration and JNI entrypoint code. | +| Java peer / managed peer | Two objects representing one interop instance, with separate runtimes and garbage collectors. JNI references and peer bookkeeping link their lifetimes. | -When a JNI object arrives in managed code, -[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -resolves its Java type to a managed target/proxy using the generated map; -[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -reuses or constructs a managed peer for its JNI reference. Conversely, the -type manager resolves managed Java-peer types to JNI type signatures when -needed. The map is about **type and peer resolution**; it does not replace -normal Java.Interop method invocation. +For example, an application can subclass a bound `Activity`: + +```csharp +using Android.App; +using Android.OS; -## Java calls managed code +namespace MyApp +{ + [Activity (Name = "my.app.MainActivity", MainLauncher = true)] + public class MainActivity : Activity + { + protected override void OnCreate (Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + // Application work here. + } + } +} +``` + +The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI +signature `(Landroid/os/Bundle;)V`. The app override need not repeat +`[Register]`: the scanner follows the registered base method through the +override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) +include both an explicitly registered `MainActivity` and an unannotated +`UserActivity` override. -The JCW is a real Java class generated for a managed peer. Its Java-visible -methods forward to `native n_*` methods, and its constructors use generated -`nctor_*` callbacks. The generator normally emits a per-class static -initializer like: +The corresponding JCW is roughly the following Java. This excerpt omits +constructor and peer-reference code; its class name, registration call, +override, and native callback name match the +[JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): ```java -static { - mono.android.Runtime.registerNatives (MyActivity.class); +package my.app; + +public class MainActivity extends android.app.Activity + implements mono.android.IGCUserPeer +{ + static { + mono.android.Runtime.registerNatives (MainActivity.class); + } + + @Override + public void onCreate (android.os.Bundle p0) + { + n_OnCreate_Landroid_os_Bundle_ (p0); + } + + public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0); } ``` -This is a representative *generated* shape, not a source file to edit. -Application and instrumentation wrappers defer registration through a -generated guarded `__md_registerNatives()` method where static initialization -would be too early. The `registerNatives` declaration itself is **handwritten** -in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java). - -At startup, -[`JNIEnvInit`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs) -installs the trimmable type and value managers and calls -`TrimmableTypeMap.RegisterNativeMethods()`. That binds -`mono/android/Runtime.registerNatives` to a managed callback using JNI -`RegisterNatives`. When a generated JCW requests registration for its -class, `TrimmableTypeMap.OnRegisterNatives` looks up that Java class in the -generated map and asks its generated `IAndroidCallableWrapper` registration -code to bind the class's `n_*` methods. ART can then call those registered -methods, which transition through generated managed JNI-ABI entrypoints to -the managed implementation. - -The [generator model](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) -records method name, JNI signature, and callback target for each native -registration. For exported or directly callable managed methods, -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) -emits an `UnmanagedCallersOnly` entrypoint that converts JNI arguments, calls -the managed method, converts its result, and handles exceptions. Other -callbacks use the generated registration's corresponding marshal target. -Do not confuse these **generated managed** entrypoints with the handwritten -Java declarations or the old native registration table. - -## Startup and peer lifetime - -CoreCLR's [native host](../../../src/native/clr/host/host.cc) captures the -Java VM at `JNI_OnLoad`, starts the managed runtime from -`Runtime.initInternal`, and enters `JNIEnvInit.Initialize`. The native -`Runtime.register` entrypoint remains a compatibility stub; TypeMap-based -native registration is handled on the managed side. - -NativeAOT instead enters -[`JavaInteropRuntime.JNI_OnLoad` / `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs), -constructs a -[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), -and calls `JNIEnvInit.InitializeNativeAotRuntime`. Both runtime paths -register the same `TrimmableTypeMap` JNI callback, but NativeAOT cannot -depend on CoreCLR's reflection-based fallback for missing mappings. +The Java name is `my/app/MainActivity`, not the assembly-qualified C# type +name. The method descriptor means one `android.os.Bundle` argument and a +`void` return; the `L...;` denotes an object reference. The generated +callback name is derived from binding metadata. `OnCreate` can call +`base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, +not a second JCW registration. + +## Build-time representation + +[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) +invokes the +[`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +on the application, references, and SDK/framework inputs. The scanner reads +managed assembly metadata, including Java type names, `[Register]` methods, +overrides of registered base/interface methods, constructors, `[Export]` +members, and manifest components. The +[`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) +validates the resulting model and feeds both the Java and managed emitters. +The app's generated JCWs do not come from the old +`JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. + +The main outputs are: + +| Output | Role | +| --- | --- | +| `typemap/java/**/*.java` | Generated JCWs; [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) emits class declarations, constructors, overrides, native declarations, registration, and `IGCUserPeer` hooks. | +| `typemap/_*.TypeMap.dll` | Per-input-assembly managed mapping attributes, proxy types, and JNI registration methods emitted by [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs). | +| `typemap/_Microsoft.Android.TypeMaps.dll` | Root assembly anchoring the per-assembly TypeMaps; its generated `TypeMapLoader.Initialize()` wires the mapping dictionaries ([`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs)). | +| `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | +| `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | + +Each per-assembly TypeMap contains assembly-level mapping attributes: a key +such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the +managed target type. The root references the per-assembly maps via +`TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, +not `.cs` files; the C# in the sections below is an explanatory translation +of emitted code, not a file that a developer should edit. See +[`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), +[`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), +and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). + +For the example Activity, think of a mapping like this **conceptual C#**: + +```csharp +// Assembly metadata, emitted as IL rather than source: +[assembly: TypeMapAttribute ("my/app/MainActivity", + typeof (MainActivityProxy), typeof (MyApp.MainActivity))] + +// The emitted ACW proxy derives from JavaPeerProxy and implements +// IAndroidCallableWrapper. Proxy names are chosen by the emitter. +``` + +There is no source-level `TypeMap` attribute for application code to write. +The real emitted `TypeMapAttribute` takes the Java key and proxy reference; +its trimmable three-argument form also takes the target reference. An +unconditional mapping has just the key and proxy reference, whereas the +target reference on a conditional mapping lets the trimming pipeline +discard entries for unused managed types. ACW proxies implement +`IAndroidCallableWrapper` to register callbacks; other proxies need not. [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -provides the generated proxy's AOT-safe peer creation hook. +provides peer creation without `Activator.CreateInstance`. + +The +[shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) +run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, +and feed TypeMap DLLs to later steps. They track generated files and remove +stale JCWs when the managed model changes. The target's stamp and output lists +matter on incremental builds: a stale compiled Java class can otherwise +outlive its managed callback. + +CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) +give the TypeMap DLLs to ILLink as trimmable inputs, with +`_Microsoft.Android.TypeMaps` as the entry assembly. For +`PublishTrimmed=true`, a **second scan of linked assemblies** regenerates +JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the +pre-trim Java set is not necessarily the packaged set. NativeAOT's +[targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) +feed the generated DLLs to ILC and use its graph plus the ACW map when +producing Java shrinker configuration. Both modes need reachable managed +mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. + +## Managed code calls Java + +Android bindings are managed wrappers for *existing* Java APIs. For example, +[`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) +uses its bound member descriptor `"reportFullyDrawn.()V"` and calls +`_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The +essential C# shape (simplified) is: + +```csharp +const string id = "reportFullyDrawn.()V"; +_members.InstanceMethods.InvokeVirtualVoidMethod (id, this, null); +``` + +Java.Interop resolves the class and method, invokes JNI with the peer's +reference, and converts values/references according to the binding. Other +bound methods use `JniArgumentValue` arrays and the appropriate virtual, +nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a +per-call method dispatch table: it is involved when Java classes and managed +peers must be mapped or created, not in place of this invocation. + +When JNI returns an object or Java passes one into managed code, +[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +uses the Java name and generated mappings to select a managed target and +proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +looks for an existing managed peer or constructs one from the JNI handle. +In the reverse direction, the type manager can obtain a JNI type signature +for a managed peer type. Do not confuse a bound `Activity` MCW with the +`MainActivity` JCW that implements Java-to-managed overrides. + +## Java code calls managed code + +### JCW declarations and per-class registration + +The generated Java `onCreate` calls its native +`n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated +`nctor_*` native callbacks, and instantiate/activate the managed peer when +appropriate. The class's static block calls +`mono.android.Runtime.registerNatives (MainActivity.class)` when the class +is first initialized. This is **per Java class**. Application and +instrumentation JCWs instead use a guarded `__md_registerNatives()` helper +called from their generated wrappers: registration in their static +initializer can be too early in startup. The declaration of +`registerNatives(Class)` in +[`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) +is handwritten Java; individual JCWs and their registration calls are +generated. + +There is no concatenated `__md_methods` string and no +`Runtime.register("managed.Type, Assembly", ...)` registration in this path. +The old JNI native-symbol mangling rules (`Java_package_Class_method...`) +matter for symbol-lookup-based JNI code, but generated app JCW callbacks +here are associated with their Java declarations through +[`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): +the Java method **name** and **signature** must match; no `Java_...` export +is required for each method. + +### Runtime startup and registration handshake -A managed `Java.Lang.Object` and its Java object are peers, not one object -shared by two garbage collectors. Generated JCWs implement the Java-side +On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures +the VM at `JNI_OnLoad`, initializes the managed runtime from +`Runtime.initInternal`, and enters +[`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). +That installs `TrimmableTypeMapTypeManager` and +`TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls +`TrimmableTypeMap.RegisterNativeMethods()`. The old native +`Runtime.register` entrypoint remains a compatibility stub, **not** the +mechanism that registers every JCW. + +NativeAOT has its own entry: +[`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) +initialize the native host and a +[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), +then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers +the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional +reflection-based peer construction path when a generated mapping is absent. +Its generated `ApplicationRegistration.registerApplications()` calls +`Runtime.registerNatives` for deferred startup types; the +[`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) +invokes it after `JavaInteropRuntime.init(...)`. + +The first native binding is for `mono.android.Runtime.registerNatives` +itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses actual C# UTF-8 string literals for its JNI name and signature: + +```csharp +fixed (byte* name = "registerNatives"u8, + sig = "(Ljava/lang/Class;)V"u8) { + var method = new JniNativeMethod (name, sig, onRegisterNatives); + JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); +} +``` + +When a JCW's static block (or deferred helper) calls this Java native +method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI +name, finds its mapping/proxy, and calls the proxy's +`IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the +class's `n_*` and `nctor_*` methods with ART. The handwritten +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +does the lookup and handoff; the *per-class* method table is in a +**generated managed TypeMap proxy**. The CoreCLR host does not supply a +table of exported native functions for these app methods. + +### Generated registration in C# terms, including UTF-8 + +The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also +implements `IAndroidCallableWrapper`. For each native method it emits a +`JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI +signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` +method stack-allocates the entries and passes a `ReadOnlySpan` +to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and +callback identifiers are illustrative, and the emitter writes IL): + +```csharp +public unsafe void RegisterNatives (JniType jniType) +{ + JniNativeMethod* entries = stackalloc JniNativeMethod [1]; + delegate* unmanaged callback = &OnCreateEntry; + entries [0] = new JniNativeMethod ( + &MethodNameUtf8, &MethodSignatureUtf8, (IntPtr) callback); + JniEnvironment.Types.RegisterNatives ( + jniType.PeerReference, + new ReadOnlySpan (entries, 1)); +} +``` + +The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) +emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in +RVA data**. It can reuse identical signature data. A representative +registration pair for the Activity example is +`n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these +identify the *native* Java declaration, not the C# method's +`GetOnCreate...Handler` connector. There is no runtime decoding of +`__md_methods` or per-callback `Java_...` export lookup. The handwritten +`registerNatives` binding above likewise supplies `byte*` name/signature +data, via C# `u8` literals. + +These name/signature bytes are not Java `String` argument marshalling. JNI's +`GetStringUTFChars`/`NewStringUTF` APIs use +[modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); +the emitter stores the registration identifiers/descriptors as UTF-8 +with explicit NUL terminators (the ASCII names/signatures above have +the same bytes in either encoding). For example, the CoreCLR +[class-name lookup](../../../src/native/clr/host/host-shared.cc) +obtains a Java class name via `GetStringUTFChars`, then converts its dots to +slashes for a TypeMap key. Separately, +[`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) +encodes method names/signatures as UTF-8 with an explicit trailing NUL +for native remapping lookup. Neither path is a substitute for marshalling +Java `String` values across JNI. + +### From JNI ABI to a C# method + +JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then +the declared arguments. Java object handles appear as pointer-sized +parameters; the generated managed entrypoints have JNI-ABI-compatible +signatures and are marked `UnmanagedCallersOnly`. For the example callback, +the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and +`IntPtr savedInstanceState`. This does **not** mean the app override accepts +`IntPtr`: its bound callback/proxy converts the `Bundle` reference and +dispatches the virtual C# `OnCreate(Bundle?)`. + +The scanner reads `[Register ("onCreate", +"(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` +on the bound method (or its base). The third string is **connector metadata**, +not the Java name of the native callback. For a normal registered binding, +the scanner uses it to find the callback's declaring type and name (such as +`n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR +signature. The +[`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +emits an `UnmanagedCallersOnly` forwarder to that existing callback, which +then calls the virtual managed method. The old path made a delegate using +`GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the +trimmable path derives callback metadata from the connector without +creating a delegate per registration. + +`[Export]` (and methods classified as directly callable) take a different +branch. The +[`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) +marks them for a generated direct dispatcher. The +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) +emits the JNI entrypoint and argument conversions, calls the managed target, +converts the return value, and copies back applicable array outputs; it +does not invoke a connector callback. For example, a Java-visible C# +method can be written as: + +```csharp +[Java.Interop.Export ("handleClick")] +public bool HandleClick (Android.Views.View view, int action) +{ + return action != 0; +} +``` + +This is the shape exercised by the +[export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). +The Java wrapper has a `native` declaration with its calculated JNI +signature; the generated managed dispatcher, not the developer's C# +method directly, is the unmanaged entrypoint. + +JNI signatures are **not** CLR method signatures. JNI `Z` is an +eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and +`Landroid/os/Bundle;` a Java object reference. The emitted entrypoint +uses JNI ABI types (a `byte` for a boolean); conversions such as +`jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# +`bool`. For normal registered callbacks the generator uses the +captured original callback signature where available: older bindings +may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. +It must not assume that the callback signature is identical to the +Java descriptor; see the scanner's +[callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). +No assembly rewriter is needed to insert separate `_mm_wrapper` methods. + +Both generated forwarders and direct dispatchers use a marshal-method +frame: `BeginMarshalMethod`, a `try` around the managed call, exception +handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in +`finally`. An arbitrary managed exception is not simply allowed to +escape across an unmanaged JNI boundary. Compare +[`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +with the +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). + +The call sequence for a Java `onCreate(Bundle)` is therefore: + +1. ART enters the generated `MainActivity.onCreate`. +2. That method calls its registered `n_OnCreate_Landroid_os_Bundle_`. +3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the + registered name/signature. For a bound override, it forwards to the + connector-derived bound callback; for a direct export it marshals and + invokes the target itself. +4. Peer lookup/activation and argument conversion supply the managed + `MainActivity` and `Bundle`; virtual dispatch runs the app override. + A call to `base.OnCreate` then goes **managed-to-Java** through + `JniPeerMembers`. + +## Peer activation and lifetime + +A Java-side constructor and a managed-side constructor do not allocate +one shared object. Generated JCW constructors call `super(...)` and, +when the actual Java class matches the wrapper, a generated +`nctor_*` callback to activate the matching managed peer. This class +check matters when a JCW is further subclassed. For some startup +types registration is deferred as described above. The generator +produces the appropriate constructor shape from its scan; compare +[`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) +and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). + +On the managed side, +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses generated mapping attributes to resolve a Java class and target +type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) +can create the managed object from a handle without late-bound +`Activator.CreateInstance`; activation also accounts for an existing +replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +resolves names/types, while +[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +checks for registered peers and handles construction/ownership. + +Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) -reference hooks; the runtime's +hooks `monodroidAddReference` and `monodroidClearReferences`. Managed [`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) -and `TrimmableTypeMapValueManager` register, find, replace, and finalize -managed peers while respecting JNI reference ownership. Disposing a managed -peer or letting collection retire a pair can remove the association; keeping -a raw JNI handle after that does not keep the old managed instance alive. - -## Trimming, AOT, and diagnosing failures - -The [CoreCLR targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) -feed the generated TypeMap assemblies to ILLink as trimmable inputs. With -`PublishTrimmed=true`, they regenerate JCWs into `typemap/linked-java/` from -the **linked** assemblies, so compiled Java matches the surviving managed -callbacks. Check `typemap/linked-java-files.txt` rather than only the -pre-trim `java-files.txt` in such a build. The -[NativeAOT targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) -feed TypeMaps to ILC and use its output plus the ACW map for Java shrinker -configuration. Both pipelines depend on reachable managed types and their -generated registrations; a method or type missing after trimming is not -fixed by restoring a historical native TypeMap path. - -When investigating: - -1. Check the build's generated TypeMap DLL list, JCW list, `acw-map.txt`, - and compiled Java sources (post-trim `linked-java/` if applicable). - Compare the actual Java class/name/signature with the generated native - registration and its managed target. -2. Check `adb logcat` for a Java `UnsatisfiedLinkError`, JNI registration - failure, or managed exception during activation; distinguish a missing - callback from a missing managed peer mapping. Inspect the runtime's - `TrimmableTypeMap` and generated proxy paths for the latter. -3. For invalid or prematurely disposed JNI object references, use - [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). For build - errors about unsupported `AndroidTypeMapImplementation` values, see +tracks add/peek/remove/finalize operations, and the GC bridge coordinates +the two collectors. Local and global JNI references have different +lifetimes: a managed wrapper may need a global reference to keep its +peer accessible after a JNI call returns. `JniHandleOwnership` records +whether an incoming reference is transferred. Calling `Dispose()` can +remove the association; a raw `IntPtr` retained afterward is not a +safe substitute for a live managed peer. For invalid reference +diagnostics, see +[Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). + +## Diagnosing a broken interop path + +Start at the boundary where the failure occurs; these files distinguish +build output, registration, and peer activation: + +1. **Missing or wrong Java class:** inspect the merged manifest, + `typemap/java/` (or CoreCLR's `typemap/linked-java/` after + trimming), `java-files.txt` / `linked-java-files.txt`, and + `acw-map.txt`. Confirm the expected Java name and base/override + signatures. A stale `.class` is not proof that a corresponding + TypeMap entry survived. +2. **Unbound native method / `UnsatisfiedLinkError`:** compare the + generated JCW's `native` method **name and JNI descriptor** with + its generated proxy registration. Check whether + `mono.android.Runtime.registerNatives` ran for the class (or its + deferred helper did), and inspect `adb logcat` for the registration + exception. Do not search for a missing `Java_...` export in this + pipeline. +3. **Wrong managed type or activation failure:** check + `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` + assembly, `TrimmableTypeMap` lookup, proxy construction, and + `JavaMarshalRegisteredPeers`. Verify whether a Java callback came + from a managed-created peer or needs activation from a Java-created + peer. On NativeAOT a missing generated mapping cannot be rescued + by CoreCLR-only reflection. +4. **Release/trim-only failure:** inspect the *linked* managed + assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and + shrinker rules. The pre-trim generated Java is not necessarily + packaged Java. The + [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) + explains target stamps and stale-file handling. +5. **JNI reference or marshal failure:** use `adb logcat` and + [the JNI reference guide](debug-jni-objrefs.md) for ownership and + collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); - duplicate Java-visible constructor signatures are reported as + duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). -The [trimmable TypeMap build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) -cover generation, trimming, NativeAOT and CoreCLR packaging, and incremental -Java output. Start there when changing generated interop behavior. +The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), +[JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), +and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) +exercise these paths. Compare their expected names/signatures against +the generated DLL and Java files before changing runtime registration. From 6fdc914884c254272771cdc8dfcd9bb4b55c3da9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:29:37 +0200 Subject: [PATCH 30/58] Let editors wrap JNI interop guide prose Keep Markdown paragraphs on one source line while preserving list, table, heading, and fenced code structure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../guides/internals/JavaJNI_Interop.md | 385 +++--------------- 1 file changed, 56 insertions(+), 329 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 542a9c09078..10d9b25ef4d 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,19 +1,8 @@ # Java and managed interoperability with the trimmable TypeMap -This is an internal guide to the interop path used by **CoreCLR and NativeAOT -Android applications**. The Java VM (ART) and the managed runtime are separate -environments in one process. JNI is the boundary in both directions: bound C# -types call Java methods through Java.Interop, and Java calls managed methods -through generated Java callable wrappers (JCWs) with registered JNI entrypoints. - -`AndroidTypeMapImplementation=trimmable` is the only supported application -TypeMap implementation. Earlier versions of this guide described a choice -between reflection/delegate-based "dynamic" registration and native -LLVM-generated marshal methods. Neither describes this path. Here, the build -generates **managed** TypeMap assemblies (including proxy and marshal code) -and Java sources; the managed runtime registers their native callbacks with -ART. There is no application-level native TypeMap lookup table, generated -`Java_...` symbol for each callback, or marshal-method callback rewriting. +This is an internal guide to the interop path used by **CoreCLR and NativeAOT Android applications**. The Java VM (ART) and the managed runtime are separate environments in one process. JNI is the boundary in both directions: bound C# types call Java methods through Java.Interop, and Java calls managed methods through generated Java callable wrappers (JCWs) with registered JNI entrypoints. + +`AndroidTypeMapImplementation=trimmable` is the only supported application TypeMap implementation. Earlier versions of this guide described a choice between reflection/delegate-based "dynamic" registration and native LLVM-generated marshal methods. Neither describes this path. Here, the build generates **managed** TypeMap assemblies (including proxy and marshal code) and Java sources; the managed runtime registers their native callbacks with ART. There is no application-level native TypeMap lookup table, generated `Java_...` symbol for each callback, or marshal-method callback rewriting. | Earlier guide | Trimmable TypeMap path | | --- | --- | @@ -51,17 +40,9 @@ namespace MyApp } ``` -The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI -signature `(Landroid/os/Bundle;)V`. The app override need not repeat -`[Register]`: the scanner follows the registered base method through the -override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) -include both an explicitly registered `MainActivity` and an unannotated -`UserActivity` override. +The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI signature `(Landroid/os/Bundle;)V`. The app override need not repeat `[Register]`: the scanner follows the registered base method through the override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) include both an explicitly registered `MainActivity` and an unannotated `UserActivity` override. -The corresponding JCW is roughly the following Java. This excerpt omits -constructor and peer-reference code; its class name, registration call, -override, and native callback name match the -[JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): +The corresponding JCW is roughly the following Java. This excerpt omits constructor and peer-reference code; its class name, registration call, override, and native callback name match the [JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): ```java package my.app; @@ -83,26 +64,11 @@ public class MainActivity extends android.app.Activity } ``` -The Java name is `my/app/MainActivity`, not the assembly-qualified C# type -name. The method descriptor means one `android.os.Bundle` argument and a -`void` return; the `L...;` denotes an object reference. The generated -callback name is derived from binding metadata. `OnCreate` can call -`base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, -not a second JCW registration. +The Java name is `my/app/MainActivity`, not the assembly-qualified C# type name. The method descriptor means one `android.os.Bundle` argument and a `void` return; the `L...;` denotes an object reference. The generated callback name is derived from binding metadata. `OnCreate` can call `base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, not a second JCW registration. ## Build-time representation -[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) -invokes the -[`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -on the application, references, and SDK/framework inputs. The scanner reads -managed assembly metadata, including Java type names, `[Register]` methods, -overrides of registered base/interface methods, constructors, `[Export]` -members, and manifest components. The -[`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) -validates the resulting model and feeds both the Java and managed emitters. -The app's generated JCWs do not come from the old -`JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. +[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) invokes the [`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) on the application, references, and SDK/framework inputs. The scanner reads managed assembly metadata, including Java type names, `[Register]` methods, overrides of registered base/interface methods, constructors, `[Export]` members, and manifest components. The [`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) validates the resulting model and feeds both the Java and managed emitters. The app's generated JCWs do not come from the old `JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. The main outputs are: @@ -114,15 +80,7 @@ The main outputs are: | `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | | `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | -Each per-assembly TypeMap contains assembly-level mapping attributes: a key -such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the -managed target type. The root references the per-assembly maps via -`TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, -not `.cs` files; the C# in the sections below is an explanatory translation -of emitted code, not a file that a developer should edit. See -[`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), -[`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), -and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). +Each per-assembly TypeMap contains assembly-level mapping attributes: a key such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the managed target type. The root references the per-assembly maps via `TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, not `.cs` files; the C# in the sections below is an explanatory translation of emitted code, not a file that a developer should edit. See [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). For the example Activity, think of a mapping like this **conceptual C#**: @@ -135,118 +93,40 @@ For the example Activity, think of a mapping like this **conceptual C#**: // IAndroidCallableWrapper. Proxy names are chosen by the emitter. ``` -There is no source-level `TypeMap` attribute for application code to write. -The real emitted `TypeMapAttribute` takes the Java key and proxy reference; -its trimmable three-argument form also takes the target reference. An -unconditional mapping has just the key and proxy reference, whereas the -target reference on a conditional mapping lets the trimming pipeline -discard entries for unused managed types. ACW proxies implement -`IAndroidCallableWrapper` to register callbacks; other proxies need not. -[`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -provides peer creation without `Activator.CreateInstance`. - -The -[shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) -run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, -and feed TypeMap DLLs to later steps. They track generated files and remove -stale JCWs when the managed model changes. The target's stamp and output lists -matter on incremental builds: a stale compiled Java class can otherwise -outlive its managed callback. - -CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) -give the TypeMap DLLs to ILLink as trimmable inputs, with -`_Microsoft.Android.TypeMaps` as the entry assembly. For -`PublishTrimmed=true`, a **second scan of linked assemblies** regenerates -JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the -pre-trim Java set is not necessarily the packaged set. NativeAOT's -[targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) -feed the generated DLLs to ILC and use its graph plus the ACW map when -producing Java shrinker configuration. Both modes need reachable managed -mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. +There is no source-level `TypeMap` attribute for application code to write. The real emitted `TypeMapAttribute` takes the Java key and proxy reference; its trimmable three-argument form also takes the target reference. An unconditional mapping has just the key and proxy reference, whereas the target reference on a conditional mapping lets the trimming pipeline discard entries for unused managed types. ACW proxies implement `IAndroidCallableWrapper` to register callbacks; other proxies need not. [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) provides peer creation without `Activator.CreateInstance`. + +The [shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, and feed TypeMap DLLs to later steps. They track generated files and remove stale JCWs when the managed model changes. The target's stamp and output lists matter on incremental builds: a stale compiled Java class can otherwise outlive its managed callback. + +CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) give the TypeMap DLLs to ILLink as trimmable inputs, with `_Microsoft.Android.TypeMaps` as the entry assembly. For `PublishTrimmed=true`, a **second scan of linked assemblies** regenerates JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the pre-trim Java set is not necessarily the packaged set. NativeAOT's [targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) feed the generated DLLs to ILC and use its graph plus the ACW map when producing Java shrinker configuration. Both modes need reachable managed mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. ## Managed code calls Java -Android bindings are managed wrappers for *existing* Java APIs. For example, -[`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) -uses its bound member descriptor `"reportFullyDrawn.()V"` and calls -`_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The -essential C# shape (simplified) is: +Android bindings are managed wrappers for *existing* Java APIs. For example, [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) uses its bound member descriptor `"reportFullyDrawn.()V"` and calls `_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The essential C# shape (simplified) is: ```csharp const string id = "reportFullyDrawn.()V"; _members.InstanceMethods.InvokeVirtualVoidMethod (id, this, null); ``` -Java.Interop resolves the class and method, invokes JNI with the peer's -reference, and converts values/references according to the binding. Other -bound methods use `JniArgumentValue` arrays and the appropriate virtual, -nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a -per-call method dispatch table: it is involved when Java classes and managed -peers must be mapped or created, not in place of this invocation. - -When JNI returns an object or Java passes one into managed code, -[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -uses the Java name and generated mappings to select a managed target and -proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -looks for an existing managed peer or constructs one from the JNI handle. -In the reverse direction, the type manager can obtain a JNI type signature -for a managed peer type. Do not confuse a bound `Activity` MCW with the -`MainActivity` JCW that implements Java-to-managed overrides. +Java.Interop resolves the class and method, invokes JNI with the peer's reference, and converts values/references according to the binding. Other bound methods use `JniArgumentValue` arrays and the appropriate virtual, nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a per-call method dispatch table: it is involved when Java classes and managed peers must be mapped or created, not in place of this invocation. + +When JNI returns an object or Java passes one into managed code, [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) uses the Java name and generated mappings to select a managed target and proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) looks for an existing managed peer or constructs one from the JNI handle. In the reverse direction, the type manager can obtain a JNI type signature for a managed peer type. Do not confuse a bound `Activity` MCW with the `MainActivity` JCW that implements Java-to-managed overrides. ## Java code calls managed code ### JCW declarations and per-class registration -The generated Java `onCreate` calls its native -`n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated -`nctor_*` native callbacks, and instantiate/activate the managed peer when -appropriate. The class's static block calls -`mono.android.Runtime.registerNatives (MainActivity.class)` when the class -is first initialized. This is **per Java class**. Application and -instrumentation JCWs instead use a guarded `__md_registerNatives()` helper -called from their generated wrappers: registration in their static -initializer can be too early in startup. The declaration of -`registerNatives(Class)` in -[`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) -is handwritten Java; individual JCWs and their registration calls are -generated. - -There is no concatenated `__md_methods` string and no -`Runtime.register("managed.Type, Assembly", ...)` registration in this path. -The old JNI native-symbol mangling rules (`Java_package_Class_method...`) -matter for symbol-lookup-based JNI code, but generated app JCW callbacks -here are associated with their Java declarations through -[`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): -the Java method **name** and **signature** must match; no `Java_...` export -is required for each method. +The generated Java `onCreate` calls its native `n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated `nctor_*` native callbacks, and instantiate/activate the managed peer when appropriate. The class's static block calls `mono.android.Runtime.registerNatives (MainActivity.class)` when the class is first initialized. This is **per Java class**. Application and instrumentation JCWs instead use a guarded `__md_registerNatives()` helper called from their generated wrappers: registration in their static initializer can be too early in startup. The declaration of `registerNatives(Class)` in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) is handwritten Java; individual JCWs and their registration calls are generated. + +There is no concatenated `__md_methods` string and no `Runtime.register("managed.Type, Assembly", ...)` registration in this path. The old JNI native-symbol mangling rules (`Java_package_Class_method...`) matter for symbol-lookup-based JNI code, but generated app JCW callbacks here are associated with their Java declarations through [`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): the Java method **name** and **signature** must match; no `Java_...` export is required for each method. ### Runtime startup and registration handshake -On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures -the VM at `JNI_OnLoad`, initializes the managed runtime from -`Runtime.initInternal`, and enters -[`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). -That installs `TrimmableTypeMapTypeManager` and -`TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls -`TrimmableTypeMap.RegisterNativeMethods()`. The old native -`Runtime.register` entrypoint remains a compatibility stub, **not** the -mechanism that registers every JCW. - -NativeAOT has its own entry: -[`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) -initialize the native host and a -[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), -then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers -the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional -reflection-based peer construction path when a generated mapping is absent. -Its generated `ApplicationRegistration.registerApplications()` calls -`Runtime.registerNatives` for deferred startup types; the -[`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) -invokes it after `JavaInteropRuntime.init(...)`. - -The first native binding is for `mono.android.Runtime.registerNatives` -itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses actual C# UTF-8 string literals for its JNI name and signature: +On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures the VM at `JNI_OnLoad`, initializes the managed runtime from `Runtime.initInternal`, and enters [`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). That installs `TrimmableTypeMapTypeManager` and `TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls `TrimmableTypeMap.RegisterNativeMethods()`. The old native `Runtime.register` entrypoint remains a compatibility stub, **not** the mechanism that registers every JCW. + +NativeAOT has its own entry: [`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) initialize the native host and a [`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional reflection-based peer construction path when a generated mapping is absent. Its generated `ApplicationRegistration.registerApplications()` calls `Runtime.registerNatives` for deferred startup types; the [`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) invokes it after `JavaInteropRuntime.init(...)`. + +The first native binding is for `mono.android.Runtime.registerNatives` itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) uses actual C# UTF-8 string literals for its JNI name and signature: ```csharp fixed (byte* name = "registerNatives"u8, @@ -256,26 +136,11 @@ fixed (byte* name = "registerNatives"u8, } ``` -When a JCW's static block (or deferred helper) calls this Java native -method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI -name, finds its mapping/proxy, and calls the proxy's -`IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the -class's `n_*` and `nctor_*` methods with ART. The handwritten -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -does the lookup and handoff; the *per-class* method table is in a -**generated managed TypeMap proxy**. The CoreCLR host does not supply a -table of exported native functions for these app methods. +When a JCW's static block (or deferred helper) calls this Java native method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI name, finds its mapping/proxy, and calls the proxy's `IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the class's `n_*` and `nctor_*` methods with ART. The handwritten [`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) does the lookup and handoff; the *per-class* method table is in a **generated managed TypeMap proxy**. The CoreCLR host does not supply a table of exported native functions for these app methods. ### Generated registration in C# terms, including UTF-8 -The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also -implements `IAndroidCallableWrapper`. For each native method it emits a -`JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI -signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` -method stack-allocates the entries and passes a `ReadOnlySpan` -to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and -callback identifiers are illustrative, and the emitter writes IL): +The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also implements `IAndroidCallableWrapper`. For each native method it emits a `JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` method stack-allocates the entries and passes a `ReadOnlySpan` to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and callback identifiers are illustrative, and the emitter writes IL): ```csharp public unsafe void RegisterNatives (JniType jniType) @@ -290,65 +155,17 @@ public unsafe void RegisterNatives (JniType jniType) } ``` -The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) -emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in -RVA data**. It can reuse identical signature data. A representative -registration pair for the Activity example is -`n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these -identify the *native* Java declaration, not the C# method's -`GetOnCreate...Handler` connector. There is no runtime decoding of -`__md_methods` or per-callback `Java_...` export lookup. The handwritten -`registerNatives` binding above likewise supplies `byte*` name/signature -data, via C# `u8` literals. - -These name/signature bytes are not Java `String` argument marshalling. JNI's -`GetStringUTFChars`/`NewStringUTF` APIs use -[modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); -the emitter stores the registration identifiers/descriptors as UTF-8 -with explicit NUL terminators (the ASCII names/signatures above have -the same bytes in either encoding). For example, the CoreCLR -[class-name lookup](../../../src/native/clr/host/host-shared.cc) -obtains a Java class name via `GetStringUTFChars`, then converts its dots to -slashes for a TypeMap key. Separately, -[`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) -encodes method names/signatures as UTF-8 with an explicit trailing NUL -for native remapping lookup. Neither path is a substitute for marshalling -Java `String` values across JNI. +The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in RVA data**. It can reuse identical signature data. A representative registration pair for the Activity example is `n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these identify the *native* Java declaration, not the C# method's `GetOnCreate...Handler` connector. There is no runtime decoding of `__md_methods` or per-callback `Java_...` export lookup. The handwritten `registerNatives` binding above likewise supplies `byte*` name/signature data, via C# `u8` literals. + +These name/signature bytes are not Java `String` argument marshalling. JNI's `GetStringUTFChars`/`NewStringUTF` APIs use [modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); the emitter stores the registration identifiers/descriptors as UTF-8 with explicit NUL terminators (the ASCII names/signatures above have the same bytes in either encoding). For example, the CoreCLR [class-name lookup](../../../src/native/clr/host/host-shared.cc) obtains a Java class name via `GetStringUTFChars`, then converts its dots to slashes for a TypeMap key. Separately, [`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) encodes method names/signatures as UTF-8 with an explicit trailing NUL for native remapping lookup. Neither path is a substitute for marshalling Java `String` values across JNI. ### From JNI ABI to a C# method -JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then -the declared arguments. Java object handles appear as pointer-sized -parameters; the generated managed entrypoints have JNI-ABI-compatible -signatures and are marked `UnmanagedCallersOnly`. For the example callback, -the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and -`IntPtr savedInstanceState`. This does **not** mean the app override accepts -`IntPtr`: its bound callback/proxy converts the `Bundle` reference and -dispatches the virtual C# `OnCreate(Bundle?)`. - -The scanner reads `[Register ("onCreate", -"(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` -on the bound method (or its base). The third string is **connector metadata**, -not the Java name of the native callback. For a normal registered binding, -the scanner uses it to find the callback's declaring type and name (such as -`n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR -signature. The -[`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -emits an `UnmanagedCallersOnly` forwarder to that existing callback, which -then calls the virtual managed method. The old path made a delegate using -`GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the -trimmable path derives callback metadata from the connector without -creating a delegate per registration. - -`[Export]` (and methods classified as directly callable) take a different -branch. The -[`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) -marks them for a generated direct dispatcher. The -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) -emits the JNI entrypoint and argument conversions, calls the managed target, -converts the return value, and copies back applicable array outputs; it -does not invoke a connector callback. For example, a Java-visible C# -method can be written as: +JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then the declared arguments. Java object handles appear as pointer-sized parameters; the generated managed entrypoints have JNI-ABI-compatible signatures and are marked `UnmanagedCallersOnly`. For the example callback, the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and `IntPtr savedInstanceState`. This does **not** mean the app override accepts `IntPtr`: its bound callback/proxy converts the `Bundle` reference and dispatches the virtual C# `OnCreate(Bundle?)`. + +The scanner reads `[Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` on the bound method (or its base). The third string is **connector metadata**, not the Java name of the native callback. For a normal registered binding, the scanner uses it to find the callback's declaring type and name (such as `n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR signature. The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) emits an `UnmanagedCallersOnly` forwarder to that existing callback, which then calls the virtual managed method. The old path made a delegate using `GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the trimmable path derives callback metadata from the connector without creating a delegate per registration. + +`[Export]` (and methods classified as directly callable) take a different branch. The [`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) marks them for a generated direct dispatcher. The [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) emits the JNI entrypoint and argument conversions, calls the managed target, converts the return value, and copies back applicable array outputs; it does not invoke a connector callback. For example, a Java-visible C# method can be written as: ```csharp [Java.Interop.Export ("handleClick")] @@ -358,125 +175,35 @@ public bool HandleClick (Android.Views.View view, int action) } ``` -This is the shape exercised by the -[export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). -The Java wrapper has a `native` declaration with its calculated JNI -signature; the generated managed dispatcher, not the developer's C# -method directly, is the unmanaged entrypoint. - -JNI signatures are **not** CLR method signatures. JNI `Z` is an -eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and -`Landroid/os/Bundle;` a Java object reference. The emitted entrypoint -uses JNI ABI types (a `byte` for a boolean); conversions such as -`jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# -`bool`. For normal registered callbacks the generator uses the -captured original callback signature where available: older bindings -may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. -It must not assume that the callback signature is identical to the -Java descriptor; see the scanner's -[callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). -No assembly rewriter is needed to insert separate `_mm_wrapper` methods. - -Both generated forwarders and direct dispatchers use a marshal-method -frame: `BeginMarshalMethod`, a `try` around the managed call, exception -handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in -`finally`. An arbitrary managed exception is not simply allowed to -escape across an unmanaged JNI boundary. Compare -[`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -with the -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). +This is the shape exercised by the [export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). The Java wrapper has a `native` declaration with its calculated JNI signature; the generated managed dispatcher, not the developer's C# method directly, is the unmanaged entrypoint. + +JNI signatures are **not** CLR method signatures. JNI `Z` is an eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and `Landroid/os/Bundle;` a Java object reference. The emitted entrypoint uses JNI ABI types (a `byte` for a boolean); conversions such as `jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# `bool`. For normal registered callbacks the generator uses the captured original callback signature where available: older bindings may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. It must not assume that the callback signature is identical to the Java descriptor; see the scanner's [callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). No assembly rewriter is needed to insert separate `_mm_wrapper` methods. + +Both generated forwarders and direct dispatchers use a marshal-method frame: `BeginMarshalMethod`, a `try` around the managed call, exception handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in `finally`. An arbitrary managed exception is not simply allowed to escape across an unmanaged JNI boundary. Compare [`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) with the [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). The call sequence for a Java `onCreate(Bundle)` is therefore: 1. ART enters the generated `MainActivity.onCreate`. 2. That method calls its registered `n_OnCreate_Landroid_os_Bundle_`. -3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the - registered name/signature. For a bound override, it forwards to the - connector-derived bound callback; for a direct export it marshals and - invokes the target itself. -4. Peer lookup/activation and argument conversion supply the managed - `MainActivity` and `Bundle`; virtual dispatch runs the app override. - A call to `base.OnCreate` then goes **managed-to-Java** through - `JniPeerMembers`. +3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the registered name/signature. For a bound override, it forwards to the connector-derived bound callback; for a direct export it marshals and invokes the target itself. +4. Peer lookup/activation and argument conversion supply the managed `MainActivity` and `Bundle`; virtual dispatch runs the app override. A call to `base.OnCreate` then goes **managed-to-Java** through `JniPeerMembers`. ## Peer activation and lifetime -A Java-side constructor and a managed-side constructor do not allocate -one shared object. Generated JCW constructors call `super(...)` and, -when the actual Java class matches the wrapper, a generated -`nctor_*` callback to activate the matching managed peer. This class -check matters when a JCW is further subclassed. For some startup -types registration is deferred as described above. The generator -produces the appropriate constructor shape from its scan; compare -[`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) -and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). - -On the managed side, -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses generated mapping attributes to resolve a Java class and target -type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -can create the managed object from a handle without late-bound -`Activator.CreateInstance`; activation also accounts for an existing -replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -resolves names/types, while -[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -checks for registered peers and handles construction/ownership. - -Every generated JCW implements the Java -[`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) -hooks `monodroidAddReference` and `monodroidClearReferences`. Managed -[`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) -tracks add/peek/remove/finalize operations, and the GC bridge coordinates -the two collectors. Local and global JNI references have different -lifetimes: a managed wrapper may need a global reference to keep its -peer accessible after a JNI call returns. `JniHandleOwnership` records -whether an incoming reference is transferred. Calling `Dispose()` can -remove the association; a raw `IntPtr` retained afterward is not a -safe substitute for a live managed peer. For invalid reference -diagnostics, see -[Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). +A Java-side constructor and a managed-side constructor do not allocate one shared object. Generated JCW constructors call `super(...)` and, when the actual Java class matches the wrapper, a generated `nctor_*` callback to activate the matching managed peer. This class check matters when a JCW is further subclassed. For some startup types registration is deferred as described above. The generator produces the appropriate constructor shape from its scan; compare [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). + +On the managed side, [`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) uses generated mapping attributes to resolve a Java class and target type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) can create the managed object from a handle without late-bound `Activator.CreateInstance`; activation also accounts for an existing replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) resolves names/types, while [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) checks for registered peers and handles construction/ownership. + +Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) hooks `monodroidAddReference` and `monodroidClearReferences`. Managed [`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) tracks add/peek/remove/finalize operations, and the GC bridge coordinates the two collectors. Local and global JNI references have different lifetimes: a managed wrapper may need a global reference to keep its peer accessible after a JNI call returns. `JniHandleOwnership` records whether an incoming reference is transferred. Calling `Dispose()` can remove the association; a raw `IntPtr` retained afterward is not a safe substitute for a live managed peer. For invalid reference diagnostics, see [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). ## Diagnosing a broken interop path -Start at the boundary where the failure occurs; these files distinguish -build output, registration, and peer activation: - -1. **Missing or wrong Java class:** inspect the merged manifest, - `typemap/java/` (or CoreCLR's `typemap/linked-java/` after - trimming), `java-files.txt` / `linked-java-files.txt`, and - `acw-map.txt`. Confirm the expected Java name and base/override - signatures. A stale `.class` is not proof that a corresponding - TypeMap entry survived. -2. **Unbound native method / `UnsatisfiedLinkError`:** compare the - generated JCW's `native` method **name and JNI descriptor** with - its generated proxy registration. Check whether - `mono.android.Runtime.registerNatives` ran for the class (or its - deferred helper did), and inspect `adb logcat` for the registration - exception. Do not search for a missing `Java_...` export in this - pipeline. -3. **Wrong managed type or activation failure:** check - `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` - assembly, `TrimmableTypeMap` lookup, proxy construction, and - `JavaMarshalRegisteredPeers`. Verify whether a Java callback came - from a managed-created peer or needs activation from a Java-created - peer. On NativeAOT a missing generated mapping cannot be rescued - by CoreCLR-only reflection. -4. **Release/trim-only failure:** inspect the *linked* managed - assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and - shrinker rules. The pre-trim generated Java is not necessarily - packaged Java. The - [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) - explains target stamps and stale-file handling. -5. **JNI reference or marshal failure:** use `adb logcat` and - [the JNI reference guide](debug-jni-objrefs.md) for ownership and - collection problems. For unsupported TypeMap settings see - [the build property](../../docs-mobile/building-apps/build-properties.md); - duplicate Java-visible constructor signatures produce - [XA4259](../../docs-mobile/messages/xa4259.md). - -The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), -[JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), -and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) -exercise these paths. Compare their expected names/signatures against -the generated DLL and Java files before changing runtime registration. +Start at the boundary where the failure occurs; these files distinguish build output, registration, and peer activation: + +1. **Missing or wrong Java class:** inspect the merged manifest, `typemap/java/` (or CoreCLR's `typemap/linked-java/` after trimming), `java-files.txt` / `linked-java-files.txt`, and `acw-map.txt`. Confirm the expected Java name and base/override signatures. A stale `.class` is not proof that a corresponding TypeMap entry survived. +2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with its generated proxy registration. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. +3. **Wrong managed type or activation failure:** check `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` assembly, `TrimmableTypeMap` lookup, proxy construction, and `JavaMarshalRegisteredPeers`. Verify whether a Java callback came from a managed-created peer or needs activation from a Java-created peer. On NativeAOT a missing generated mapping cannot be rescued by CoreCLR-only reflection. +4. **Release/trim-only failure:** inspect the *linked* managed assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and shrinker rules. The pre-trim generated Java is not necessarily packaged Java. The [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) explains target stamps and stale-file handling. +5. **JNI reference or marshal failure:** use `adb logcat` and [the JNI reference guide](debug-jni-objrefs.md) for ownership and collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). + +The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), [JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) exercise these paths. Compare their expected names/signatures against the generated DLL and Java files before changing runtime registration. From 0b3db5b55b12e529f03f540e2c6f8a052b397141 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:49:39 +0200 Subject: [PATCH 31/58] Document unsupported explicit Java type registration Clarify the trimmable TypeMap behavior after the parent runtime review fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 10d9b25ef4d..0984361b13d 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -200,6 +200,8 @@ Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtim Start at the boundary where the failure occurs; these files distinguish build output, registration, and peer activation: +[`Java.Interop.TypeManager.RegisterType`](../../../src/Mono.Android/Java.Interop/TypeManager.cs) is not a fallback for a missing generated mapping: it throws `NotSupportedException` with the trimmable TypeMap. Give the peer a discoverable Java name (`[Register]` for a peer type, or the component `Name` attribute used above) so the scanner can generate the mapping instead. + 1. **Missing or wrong Java class:** inspect the merged manifest, `typemap/java/` (or CoreCLR's `typemap/linked-java/` after trimming), `java-files.txt` / `linked-java-files.txt`, and `acw-map.txt`. Confirm the expected Java name and base/override signatures. A stale `.class` is not proof that a corresponding TypeMap entry survived. 2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with its generated proxy registration. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. 3. **Wrong managed type or activation failure:** check `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` assembly, `TrimmableTypeMap` lookup, proxy construction, and `JavaMarshalRegisteredPeers`. Verify whether a Java callback came from a managed-created peer or needs activation from a Java-created peer. On NativeAOT a missing generated mapping cannot be rescued by CoreCLR-only reflection. From 79e0555a70f704679508444fef65b733ca55d953 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 14:28:07 +0200 Subject: [PATCH 32/58] Document app-generated platform JCWs Clarify that trimmable app builds generate platform wrappers rather than using a prebuilt reference-pack jar, alongside the selected runtime support jar. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 0984361b13d..25601fccb2c 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -80,6 +80,8 @@ The main outputs are: | `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | | `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | +The app build also generates needed **platform JCWs** from framework inputs alongside user JCWs: for example, the [build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) check for `typemap/java/android/app/ActivityTracker.java` and its `Runtime.registerNatives` call. The [reference pack](../../../build-tools/create-packs/Microsoft.Android.Ref.proj) no longer ships prebuilt `mono.android.jar` or `mono.android.dex`. The handwritten Java runtime support instead comes from `java_runtime_trimmable.jar`, selected by [`_CollectRuntimeJarFilenames`](../../../src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets); this is not a jar of pre-generated platform JCWs. `ApplicationRegistration.java` is generated separately during the app build. + Each per-assembly TypeMap contains assembly-level mapping attributes: a key such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the managed target type. The root references the per-assembly maps via `TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, not `.cs` files; the C# in the sections below is an explanatory translation of emitted code, not a file that a developer should edit. See [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). For the example Activity, think of a mapping like this **conceptual C#**: From 9d7e9541644e0c5b456b8d2c92b9d6fdef26d16e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 10:09:19 +0200 Subject: [PATCH 33/58] Remove unsupported typemap tests from Android runs Omit obsolete reflection/native-map fixtures from the trimmable Android test assembly while retaining their JVM coverage and the unrelated mixed-file tests. Drop defunct category exclusions and test-only legacy build wiring. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../skills/tests/references/test-catalog.md | 2 +- .../InvokeVirtualFromConstructorTests.cs | 1 - .../Java.Interop/JavaManagedGCBridgeTests.cs | 3 ++- .../Java.Interop/JavaObjectArrayTest.cs | 3 ++- .../Java.Interop/JniInstanceMethodIDTest.cs | 1 - .../Java.Interop/JniPeerMembersTests.cs | 7 ++++--- .../JniRuntime.JniTypeManagerTests.cs | 3 ++- .../JniRuntimeJniValueManagerContract.cs | 4 ---- .../Java.Interop/JniRuntimeTest.cs | 3 ++- .../JniValueMarshalerContractTests.cs | 6 ++++-- .../Java.Interop/MethodBindingTests.cs | 1 - .../Java.Interop/TestTypeTests.cs | 1 - .../Java.Interop-Tests.NET.csproj | 21 +++++-------------- .../Mono.Android.NET-Tests.csproj | 3 --- 14 files changed, 22 insertions(+), 37 deletions(-) diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md index 2adcf28b912..a512cef82b6 100644 --- a/.github/skills/tests/references/test-catalog.md +++ b/.github/skills/tests/references/test-catalog.md @@ -138,7 +138,7 @@ Device: **Yes** The `Mono.Android.NET-Tests.csproj` dynamically excludes categories based on runtime: - **CoreCLR runtime**: Excludes `CoreCLRIgnore`, `NTLM` -- **NativeAOT runtime**: Excludes `NativeAOTIgnore`, `SSL`, `NTLM`, `Export`, `NativeTypeMap` +- **NativeAOT runtime**: Excludes `NativeAOTIgnore`, `SSL`, `NTLM` Other categories: `SSL`, `InetAccess`, `JavaList`, `RuntimeConfig`, `Intune`, `NTLM` diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs index 949f1902997..daf35ea5a15 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs @@ -8,7 +8,6 @@ namespace Java.InteropTests { [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] #if !__ANDROID__ // We want stability around the CallVirtualFromConstructorDerived static fields [NonParallelizable] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs index 79b64ef2fd5..1635a3d4520 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs @@ -8,8 +8,8 @@ namespace Java.InteropTests { +#if !__ANDROID__ [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] public class JavaManagedGCBridgeTests : JavaVMFixture { #if !NO_GC_BRIDGE_SUPPORT @@ -54,6 +54,7 @@ static void SetupLinks (JavaObjectArray array, out WeakRef child = new WeakReference (b, true); } } +#endif // !__ANDROID__ [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] public class CrossReferenceBridge : JavaObject { diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs index e5d2bc2fa98..695597da7ae 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs @@ -165,8 +165,8 @@ public void ObjectArrayType () } } +#if !__ANDROID__ [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] public class JavaObjectArray_object_ContractTest : JavaObjectArrayContractTest { static readonly object a = new object (); @@ -201,4 +201,5 @@ public void ObjectArrayType () Dispose (c); } } +#endif // !__ANDROID__ } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs index bce118ca8e7..2949f14286c 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs @@ -11,7 +11,6 @@ public class JniInstanceMethodIDTest : JavaVMFixture { // https://code.google.com/p/android/issues/detail?id=65710 [Test] - [Category ("TrimmableTypeMapUnsupported")] public unsafe void CallNonvirtualVoidMethod_WithBaseMethodIDAndDerivedType () { using (var b = new JniType ("net/dot/jni/test/CallNonvirtualBase")) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index 40ed2dc5390..83ebf2ece31 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -39,8 +39,8 @@ public void HandledReplacementTypeMissDoesNotUseStringFallback () } #endif // !ANDROID +#if !__ANDROID__ [Test] - [Category ("TrimmableTypeMapUnsupported")] public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () { Assert.IsNull (GetInstanceMethods (MyString._members.InstanceMethods)); @@ -53,7 +53,6 @@ public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () } [Test] - [Category ("TrimmableTypeMapUnsupported")] public void ConcurrentFirstUsePublishesSingleInstanceMethodCache () { var members = new JniPeerMembers (MyString.JniTypeName, typeof (MyString)); @@ -73,6 +72,7 @@ public void ConcurrentFirstUsePublishesSingleInstanceMethodCache () JniPeerMembers.Dispose (members); } } +#endif // !__ANDROID__ [Test] public void PeerMemberCachesAreInitiallyNull () @@ -226,15 +226,16 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () } } +#if !__ANDROID__ [Test] [Category ("NativeAOTIgnore")] - [Category ("TrimmableTypeMapUnsupported")] public void ReplacementTypeUsedForMethodLookup () { using var o = new RenameClassDerived (); int r = o.hashCode(); Assert.AreEqual (33, r); } +#endif // !__ANDROID__ [Test] [Category ("NativeAOTIgnore")] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs index f2dd684a8dd..8a5ef10a2b8 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs @@ -60,8 +60,8 @@ public void ReplacementTypeInfoSupportsStringAndUtf8Results () } } +#if !__ANDROID__ [Test] - [Category ("TrimmableTypeMapUnsupported")] [RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] [RequiresUnreferencedCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")] public void GetInvokerType () @@ -85,6 +85,7 @@ public MyTypeManager () { } } +#endif // !__ANDROID__ class StringReplacementTypeManager : JniRuntime.JniTypeManager { diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs index a3343de62a0..b52a53edab2 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs @@ -14,12 +14,9 @@ namespace Java.InteropTests { - // Android doesn't support `[NonParallelizable]`, but runs tests sequentially by default. #if !__ANDROID__ // Modifies JniRuntime.valueManager instance field; can't be done in parallel [NonParallelizable] -#endif // !__ANDROID__ - [Category ("TrimmableTypeMapUnsupported")] public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture { [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] @@ -473,7 +470,6 @@ public abstract class JniRuntimeJniValueManagerContract< protected override Type ValueManagerType => typeof (T); } -#if !__ANDROID__ [TestFixture] public class JniRuntimeJniValueManagerContract_NoGCIntegration : JniRuntimeJniValueManagerContract { } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs index f28d24727aa..d7af269401a 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs @@ -72,13 +72,14 @@ public void CreateJavaVMWithNullBuilder () Assert.Throws (() => new JavaVMWithNullBuilder ()); } +#if !__ANDROID__ [Test] - [Category ("TrimmableTypeMapUnsupported")] public void BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault () { var types = JniRuntime.CurrentRuntime.TypeManager.GetTypes (new JniTypeSignature (ManagedPeer.JniTypeName)); Assert.IsTrue (types.Contains (typeof (ManagedPeer))); } +#endif // !__ANDROID__ class JavaVMWithNullBuilder : JniRuntime { public JavaVMWithNullBuilder () diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs index b241836a9d9..1f704229448 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs @@ -14,7 +14,7 @@ namespace Java.InteropTests { - [Category ("TrimmableTypeMapUnsupported")] +#if !__ANDROID__ public abstract class JniValueMarshalerContractTests< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T @@ -704,6 +704,7 @@ protected override string GetExpectedReturnValueFromManagedExpression (string jv }}"; } } +#endif // !__ANDROID__ [JniValueMarshaler (typeof (DemoValueTypeValueMarshaler))] struct DemoValueType { @@ -760,11 +761,12 @@ public override void DestroyGenericArgumentState (DemoValueType value, ref JniVa } } +#if !__ANDROID__ [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] class JniValueMarshaler_DemoValueType_ContractTests : JniValueMarshalerContractTests { protected override DemoValueType Value {get {return new DemoValueType (42);}} protected override bool IsJniValueType {get {return true;}} } +#endif // !__ANDROID__ } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs index 05e8156fa91..de783870094 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs @@ -7,7 +7,6 @@ namespace Java.InteropTests { [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] public class MethodBindingTests : JavaVMFixture { // diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs index b65e6f08412..3ae85674ca7 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs @@ -9,7 +9,6 @@ namespace Java.InteropTests { #if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] public class TestTypeTests : JavaVMFixture { int lrefStartCount; diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj index 21920b4b9dd..fd25f6e85cb 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj @@ -27,25 +27,14 @@ - - - - - + + + + - - - - - + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index e3215dc6dc9..42c688e8ffe 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -9,7 +9,6 @@ Exe true $(XamarinAndroidSourcePath)product.snk - <_SkipJniAddNativeMethodRegistrationAttributeScan>True true $(XamarinAndroidSourcePath)bin\Test$(Configuration) false @@ -80,8 +79,6 @@ - - From 32573f739ee0e9807addaaf7126bcd4faf54c80c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 11:29:56 +0200 Subject: [PATCH 34/58] Drop desktop-only Java.Interop test stages Stop invoking the standalone JVM/tooling/NativeAOT test stage in public, internal, and official CI. Remove the orphaned GC-bridge test and fixture rather than guarding it by platform; Android interop tests remain in device lanes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../automation/azure-pipelines-internal.yaml | 12 -- .../automation/azure-pipelines-public.yaml | 14 -- build-tools/automation/azure-pipelines.yaml | 11 -- .../stage-java-interop-tests.yaml | 131 ------------------ .../Java.Interop-Tests.csproj | 1 - .../Java.Interop/JavaManagedGCBridgeTests.cs | 70 ---------- .../Java.Interop/JavaVMFixture.cs | 1 - .../dot/jni/test/CrossReferenceBridge.java | 32 ----- 8 files changed, 272 deletions(-) delete mode 100644 build-tools/automation/yaml-templates/stage-java-interop-tests.yaml delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java diff --git a/build-tools/automation/azure-pipelines-internal.yaml b/build-tools/automation/azure-pipelines-internal.yaml index c889c888558..e18ed162391 100644 --- a/build-tools/automation/azure-pipelines-internal.yaml +++ b/build-tools/automation/azure-pipelines-internal.yaml @@ -820,18 +820,6 @@ extends: parameters: condition: true - # Java.Interop Tests Stage - - template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - windowsPool: - name: $(NetCoreInternalPoolName) - image: $(WindowsPoolImageNetCoreInternal) - os: windows - macPool: - name: Azure Pipelines - vmImage: $(HostedMacImage) - os: macOS - # MAUI Tests Stage - stage: maui_tests displayName: MAUI Tests diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index c6fa657ba10..d5b35ce7500 100644 --- a/build-tools/automation/azure-pipelines-public.yaml +++ b/build-tools/automation/azure-pipelines-public.yaml @@ -432,20 +432,6 @@ stages: parameters: condition: true -# Java.Interop Tests Stage -- template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - windowsPool: - name: $(NetCorePublicPoolName) - demands: - - ImageOverride -equals $(WindowsPoolImageNetCorePublic) - os: windows - macPool: - name: AcesShared - demands: - - ImageOverride -equals ACES_VM_SharedPool_Tahoe - os: macOS - # Android Tools Tests Stage - template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self parameters: diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml index ec05b713c2e..7463c4434e8 100644 --- a/build-tools/automation/azure-pipelines.yaml +++ b/build-tools/automation/azure-pipelines.yaml @@ -106,17 +106,6 @@ extends: parameters: usesCleanImages: ${{ parameters.macTestAgentsUseCleanImages }} - - template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - # The internal Xamarin.Android pipeline uses 1ES Pipeline Templates, which - # require Windows jobs to run on a 1ES-hosted pool. macOS is fine on the - # hosted Azure Pipelines pool as long as `os: macOS` is set (which is now - # the template default). - windowsPool: - name: MAUI-1ESPT - image: $(WindowsPoolImage1ESPT) - os: windows - - template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self parameters: use1ESTemplate: true diff --git a/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml b/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml deleted file mode 100644 index 92aff51cf19..00000000000 --- a/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml +++ /dev/null @@ -1,131 +0,0 @@ -# Java.Interop tests stage -# -# Mirrors the two jobs from -# external/Java.Interop/build-tools/automation/azure-pipelines.yaml -# (windows_dotnet_build, mac_dotnet_build) so that the Java.Interop -# build + tests continue to run as part of dotnet/android CI now that -# Java.Interop has been merged in-tree. -# -# Referenced from both: -# - build-tools/automation/azure-pipelines.yaml (official / 1ES) -# - build-tools/automation/azure-pipelines-public.yaml (public PR validation) - -parameters: -- name: stageName - type: string - default: java_interop_tests -- name: dependsOn - type: object - default: [] -- name: condition - type: string - default: succeeded() -- name: windowsPool - type: object - # NOTE: This default is a Microsoft-hosted Azure Pipelines pool, which is NOT - # allowed by 1ES Pipeline Templates in Official mode. Callers that extend the - # 1ES Official template (e.g. build-tools/automation/azure-pipelines.yaml) must - # override this with a 1ES-hosted pool like `MAUI-1ESPT`. - default: - name: Azure Pipelines - vmImage: $(HostedWinImage) - os: windows -- name: macPool - type: object - default: - name: Azure Pipelines - vmImage: $(HostedMacImage) - os: macOS - -stages: -- stage: ${{ parameters.stageName }} - displayName: Java.Interop Tests - dependsOn: ${{ parameters.dependsOn }} - condition: ${{ parameters.condition }} - variables: - # Variables consumed by external/Java.Interop/build-tools/automation/templates/*.yaml. - # Defined at stage scope so they don't conflict with dotnet/android-wide - # variables (e.g. DotNetTargetFramework=net11.0). - DotNetCoreVersion: $(DotNetSdkVersion).x - DotNetTargetFramework: $(DotNetStableTargetFramework) - NetCoreTargetFrameworkPathSuffix: -$(DotNetStableTargetFramework) - Build.Configuration: Release - RunningOnCI: true - # Cap the JDK that external/Java.Interop's Prepare.targets picks when it - # calls the JdkInfo bootstrap task. Recent hosted agent images ship Java 25 - # as the default JDK, and Gradle 8.12 (used by java-source-utils and the - # Bytecode-Tests kotlin-gradle project) crashes with "Unsupported class - # file major version 69" on it. JdkInfo reads JI_MAX_JDK from the env. - JI_MAX_JDK: 21 - jobs: - - # Check - "Xamarin.Android (Java.Interop Tests Windows - .NET)" - - job: java_interop_windows_dotnet_build - displayName: Windows - .NET - pool: ${{ parameters.windowsPool }} - timeoutInMinutes: 60 - workspace: - clean: all - steps: - - checkout: self - submodules: recursive - clean: true - - - template: /external/Java.Interop/build-tools/automation/templates/install-dependencies.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-build.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-tests.yaml@self - parameters: - runNativeDotnetTests: true - nativeAotRid: win-x64 - platformName: .NET - Windows - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-dirty-tree.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-issue.yaml@self - - # Check - "Xamarin.Android (Java.Interop Tests Mac - .NET)" - - job: java_interop_mac_dotnet_build - displayName: Mac - .NET - pool: ${{ parameters.macPool }} - timeoutInMinutes: 20 - workspace: - clean: all - steps: - - checkout: self - submodules: recursive - clean: true - - - bash: | - set -e - if ! command -v cmake >/dev/null 2>&1 ; then - brew install cmake - fi - displayName: Ensure cmake is installed - - # The hosted macOS pool architecture can change between Intel (osx-x64) and - # Apple Silicon (osx-arm64), so detect it at runtime and publish/run the - # NativeAOT sample for the matching RID. Otherwise the produced .dylib fails - # to dlopen with an "incompatible architecture" error. - - bash: | - if [ "$(Agent.OSArchitecture)" = "ARM64" ]; then - echo "##vso[task.setvariable variable=NativeAOTRid]osx-arm64" - else - echo "##vso[task.setvariable variable=NativeAOTRid]osx-x64" - fi - displayName: Determine NativeAOT RID - - - template: /external/Java.Interop/build-tools/automation/templates/install-dependencies.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-build.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-tests.yaml@self - parameters: - runNativeTests: true - nativeAotRid: $(NativeAOTRid) - platformName: .NET - MacOS - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-dirty-tree.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-issue.yaml@self diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 1d43a2ca427..864e5046c8a 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -32,7 +32,6 @@ - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs deleted file mode 100644 index 1635a3d4520..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - -#if !__ANDROID__ - [TestFixture] - public class JavaManagedGCBridgeTests : JavaVMFixture { - -#if !NO_GC_BRIDGE_SUPPORT - // https://github.com/mono/mono/blob/98d2314/mono/tests/sgen-bridge-xref.cs - [Test] - public void CrossReferences () - { - using (var array = new JavaObjectArray (2)) { - WeakReference root = null, child = null; - var t = new Thread (() => SetupLinks (array, out root, out child)); - t.Start (); - t.Join (); - - JniEnvironment.Runtime.ValueManager.CollectPeers (); - CrossReferenceBridge a, b; - a = b = null; - Console.WriteLine ("try get A {0}", root.TryGetTarget (out a)); - Console.WriteLine ("try get B {0}", child.TryGetTarget (out b)); - Console.WriteLine ("a is null {0}", a == null); - Console.WriteLine ("b is null {0}", b == null); - - Assert.IsNotNull (a); - Assert.IsNotNull (b); - } - } -#endif // !NO_GC_BRIDGE_SUPPORT - - static void SetupLinks (JavaObjectArray array, out WeakReference root, out WeakReference child) - { - var a = new CrossReferenceBridge () { - id = "bridge", - }; - var b = new CrossReferenceBridge () { - id = "child", - }; - a.link.Add (b); - - array [0] = a; - array [1] = b; - - root = new WeakReference (a, true); - child = new WeakReference (b, true); - } - } -#endif // !__ANDROID__ - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - public class CrossReferenceBridge : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/CrossReferenceBridge"; - - public string id; - public List link = new List (); - - protected override void Dispose (bool disposing) - { - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 8b5459df5eb..6a753b5ae01 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -53,7 +53,6 @@ enum ReplacementMethodStorage { [AnotherJavaInterfaceImpl.JniTypeName] = typeof (AnotherJavaInterfaceImpl), [CallVirtualFromConstructorBase.JniTypeName] = typeof (CallVirtualFromConstructorBase), [CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived), - [CrossReferenceBridge.JniTypeName] = typeof (CrossReferenceBridge), [GetThis.JniTypeName] = typeof (GetThis), [IAndroidInterface.JniTypeName] = typeof (IAndroidInterface), [IJavaInterface.JniTypeName] = typeof (IJavaInterface), diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java deleted file mode 100644 index a2c915bfe77..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CrossReferenceBridge implements GCUserPeerable { - - static final String assemblyQualifiedName = "Java.InteropTests.CrossReferenceBridge, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public CrossReferenceBridge () { - if (CrossReferenceBridge.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} From 34b9d4aa835f7682aab2ada2274894e5142f47fc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 13:44:17 +0200 Subject: [PATCH 35/58] Remove JVM-only Java.Interop test coverage Retire the desktop-only test cases and Java fixtures now that Android is the supported test host. Keep Android test scenarios and their shared marshaler fixture, and remove unused jar inputs and remapping rules. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop-Tests.csproj | 6 - .../CallVirtualFromConstructorBase.cs | 55 -- .../CallVirtualFromConstructorDerived.cs | 101 --- .../InvokeVirtualFromConstructorTests.cs | 155 ---- .../Java.Interop/JavaObjectArrayTest.cs | 37 - .../Java.Interop/JavaObjectTest.cs | 13 +- .../JavaPeerableExtensionsTests.cs | 4 - .../Java.Interop/JavaVMFixture.cs | 55 +- .../Java.Interop/JniInstanceMethodIDTest.cs | 40 - .../Java.Interop/JniPeerMembersTests.cs | 171 ---- .../JniRuntime.JniTypeManagerTests.cs | 117 --- .../JniRuntime.JniValueManagerTests.cs | 149 ---- .../JniRuntimeJniValueManagerContract.cs | 531 ------------ .../Java.Interop/JniRuntimeTest.cs | 54 -- .../Java.Interop/JniTypeManagerTests.cs | 12 - .../JniValueMarshalerAttributeTests.cs | 53 +- .../JniValueMarshalerContractTests.cs | 772 ------------------ .../Java.Interop/MethodBindingTests.cs | 78 -- .../jni/test/AnotherJavaInterfaceImpl.java | 13 - .../test/CallVirtualFromConstructorBase.java | 38 - .../CallVirtualFromConstructorDerived.java | 47 -- .../net/dot/jni/test/RenameClassBase1.java | 39 - .../net/dot/jni/test/RenameClassBase2.java | 45 - .../net/dot/jni/test/RenameClassDerived.java | 45 - .../Java.Interop-Tests.NET.csproj | 7 - .../Mono.Android-Tests/Remaps.xml | 9 - 26 files changed, 54 insertions(+), 2592 deletions(-) delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 864e5046c8a..bdf902db659 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -31,19 +31,13 @@ - - - - - - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs deleted file mode 100644 index ce298734b93..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; -namespace Java.InteropTests { - - [JniTypeSignature (CallVirtualFromConstructorBase.JniTypeName, GenerateJavaPeer=false)] - public class CallVirtualFromConstructorBase : JavaObject { - - internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorBase"; - readonly static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorBase)); - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - public unsafe CallVirtualFromConstructorBase (int value) - : this (value, useNewObject: false) - { - } - - public unsafe CallVirtualFromConstructorBase (int value, bool useNewObject) - : this (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - if (PeerReference.IsValid) - return; - - const string __id = "(I)V"; - - if (useNewObject) { - var ctors = JniPeerMembers.InstanceMethods.GetConstructorsForType (GetType ()); - var init = ctors.GetConstructor (__id); - - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue (value); - var lref = JniEnvironment.Object.NewObject (ctors.JniPeerType.PeerReference, init, __args); - Construct (ref lref, JniObjectReferenceOptions.CopyAndDispose); - return; - } - var peer = JniPeerMembers.InstanceMethods.StartGenericCreateInstance (__id, GetType (), value); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - JniPeerMembers.InstanceMethods.FinishGenericCreateInstance (__id, this, value); - } - - public CallVirtualFromConstructorBase (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref reference, options) - { - } - - public virtual void CalledFromConstructor (int value) - { - _members.InstanceMethods.InvokeGenericVirtualVoidMethod ("calledFromConstructor.(I)V", this, value); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs deleted file mode 100644 index 739b85e08c9..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs +++ /dev/null @@ -1,101 +0,0 @@ -#nullable enable - -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; - -namespace Java.InteropTests -{ - [JniTypeSignature (CallVirtualFromConstructorDerived.JniTypeName, GenerateJavaPeer=false)] - public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase { - new internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorDerived"; - static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorDerived)); - - [JniAddNativeMethodRegistrationAttribute] - static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) - { - args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (CalledFromConstructorMarshalMethod)CalledFromConstructorHandler)); - } - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - int calledValue; - - public bool InvokedConstructor; - - public CallVirtualFromConstructorDerived (int value) - : this (value, useNewObject: false) - { - } - - public CallVirtualFromConstructorDerived (int value, bool useNewObject) - : base (value, useNewObject) - { - InvokedConstructor = true; - - if (useNewObject && calledValue != 0) { - // calledValue was set on a *different* instance! So it's 0 here. - throw new ArgumentException ( - string.Format ("value '{0}' doesn't match expected value '{1}'.", value, 0), - "value"); - } - if (!useNewObject && value != calledValue) - throw new ArgumentException ( - string.Format ("value '{0}' doesn't match expected value '{1}'.", value, calledValue), - "value"); - } - - public bool InvokedActivationConstructor; - - public static CallVirtualFromConstructorDerived? Intermediate_FromCalledFromConstructor; - public static CallVirtualFromConstructorDerived? Intermediate_FromActivationConstructor; - - public CallVirtualFromConstructorDerived (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref reference, options) - { - InvokedActivationConstructor = true; - - Intermediate_FromActivationConstructor = this; - } - - public bool Called; - - public override void CalledFromConstructor (int value) - { - Called = true; - calledValue = value; - - Intermediate_FromCalledFromConstructor = this; - } - - public static unsafe CallVirtualFromConstructorDerived NewInstance (int value) - { - JniArgumentValue* args = stackalloc JniArgumentValue [1]; - args [0] = new JniArgumentValue (value); - var o = _members.StaticMethods.InvokeObjectMethod ("newInstance.(I)Lnet/dot/jni/test/CallVirtualFromConstructorDerived;", args); - return JniEnvironment.Runtime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)!; - } - - delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value); - static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int value) - { - var envp = new JniTransition (jnienv); - try { - var r_self = new JniObjectReference (n_self); - var self = JniEnvironment.Runtime.ValueManager.GetValue(ref r_self, JniObjectReferenceOptions.Copy)!; - self.CalledFromConstructor (value); - self.DisposeUnlessReferenced (); - } - catch (Exception e) when (JniEnvironment.Runtime.ExceptionShouldTransitionToJni (e)) { - envp.SetPendingException (e); - } - finally { - envp.Dispose (); - } - } - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs deleted file mode 100644 index daf35ea5a15..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] -#if !__ANDROID__ - // We want stability around the CallVirtualFromConstructorDerived static fields - [NonParallelizable] -#endif // !__ANDROID__ - public class InvokeVirtualFromConstructorTests : JavaVMFixture - { - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateManagedInstanceFirst_WithAllocObject () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = new CallVirtualFromConstructorDerived (42); - Assert.IsTrue ( - t.Called, - "CalledFromConstructor method override should have been called."); - Assert.IsFalse ( - t.InvokedActivationConstructor, - "Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.IsNull (acIntermediate, - "Activation Constructor should not have been called, because of AllocObject semantics"); - Assert.AreSame (t, cfIntermediate, - "Expected t and cfIntermediate to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - - static void Dispose (ref T peer) - where T : class, IJavaPeerable - { - if (peer == null) - return; - - peer.Dispose (); - peer = null; - } - - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateManagedInstanceFirst_WithNewObject () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = new CallVirtualFromConstructorDerived (42, useNewObject: true); - Assert.IsFalse ( - t.Called, - "CalledFromConstructor method override was called on a different instance."); - Assert.IsFalse ( - t.InvokedActivationConstructor, - "Activation Constructor should not have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.IsNotNull (acIntermediate, - "Activation Constructor should have been called, because of NewObject"); - Assert.IsTrue ( - acIntermediate.Called, - "CalledFromConstructor method override should have been called on acIntermediate."); - Assert.IsTrue ( - acIntermediate.InvokedActivationConstructor, - "Activation Constructor should have been called on intermediate instance, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.AreNotSame (t, acIntermediate, - "Expected t and registered to be different instances; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}"); - Assert.AreNotSame (t, cfIntermediate, - "Expected t and cfIntermediate to be different instances; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - Assert.AreSame (acIntermediate, cfIntermediate, - "Expected acIntermediate and cfIntermediate to be the same instance; " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateJavaInstanceFirst () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = CallVirtualFromConstructorDerived.NewInstance (42); - - Assert.IsTrue ( - t.Called, - "CalledFromConstructor method override should have been called."); - Assert.IsTrue ( - t.InvokedActivationConstructor, - "Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.AreSame (t, acIntermediate, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}"); - Assert.AreSame (t, cfIntermediate, - "Expected t and cfIntermediate to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs index 695597da7ae..c352abdf3f5 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs @@ -165,41 +165,4 @@ public void ObjectArrayType () } } -#if !__ANDROID__ - [TestFixture] - public class JavaObjectArray_object_ContractTest : JavaObjectArrayContractTest { - static readonly object a = new object (); - - protected override object CreateValueA () {return a;} - protected override object CreateValueB () {return 42;} - - int grefStartCount; - - [OneTimeSetUp] - public void BeginCheckGlobalRefCount () - { - // So that the JavaProxyObject.TypeRef GREF isn't counted. - using (var o = new JavaObjectArray (1)) - o [0] = a; - grefStartCount = JniEnvironment.Runtime.GlobalReferenceCount; - } - - [OneTimeTearDown] - public void EndCheckGlobalRefCount () - { - int gref = JniEnvironment.Runtime.GlobalReferenceCount; - Assert.IsTrue (gref <= (grefStartCount), - string.Format ("JNI global references: grefStartCount={0}; gref={1}", grefStartCount, gref)); - JniEnvironment.Runtime.ValueManager.CollectPeers (); - } - - [Test] - public void ObjectArrayType () - { - var c = CreateCollection (new object[0]); - Assert.AreEqual ("[Ljava/lang/Object;", ((IJavaPeerable) c).GetJniTypeName ()); - Dispose (c); - } - } -#endif // !__ANDROID__ } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs index 1d308fc945a..49bd8b16dd0 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs @@ -187,13 +187,7 @@ public void Ctor_Exceptions () #if __ANDROID__ Assert.Throws (() => new JavaObjectWithMissingJavaPeer ()).Dispose (); -#else // !__ANDROID__ - // Note: `JavaObjectWithNoJavaPeer` creation works on Android because tooling provides all - // typemap entries. On desktop, we use the hardcoded dictionary in JavaVMFixture, which - // deliberately *lacks* an entry for `JavaObjectWithNoJavaPeer`. - Assert.Throws (() => new JavaObjectWithNoJavaPeer ()); - Assert.Throws (() => new JavaObjectWithMissingJavaPeer ()).Dispose (); -#endif // !__ANDROID__ +#endif // __ANDROID__ } [Test] @@ -224,11 +218,6 @@ public void DisposeAccessesThis () } } -#if !__ANDROID__ - class JavaObjectWithNoJavaPeer : JavaObject { - } -#endif // !__ANDROID__ - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] class JavaObjectWithMissingJavaPeer : JavaObject { internal const string JniTypeName = "__this__/__type__/__had__/__better__/__not__/__Exist__"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs index 48e6580cd8c..367a8e050cd 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs @@ -23,10 +23,6 @@ public void JavaAs_Exceptions () using var o = new JavaObject (ref r, JniObjectReferenceOptions.Copy); // MyJavaInterfaceImpl doesn't provide an activation constructor Assert.Throws(() => o.JavaAs()); -#if !__ANDROID__ - // JavaObjectWithNoJavaPeer has no Java peer - Assert.Throws(() => v.JavaAs()); -#endif // !__ANDROID__ } [Test] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 6a753b5ae01..fac3dcf670d 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; -using System.Threading; using Java.Interop; @@ -48,11 +47,6 @@ enum ReplacementMethodStorage { [TestType.JniTypeName] = typeof (TestType), #endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT [GenericHolder.JniTypeName] = typeof (GenericHolder<>), - [RenameClassBase.JniTypeName] = typeof (RenameClassBase), - [RenameClassDerived.JniTypeName] = typeof (RenameClassDerived), - [AnotherJavaInterfaceImpl.JniTypeName] = typeof (AnotherJavaInterfaceImpl), - [CallVirtualFromConstructorBase.JniTypeName] = typeof (CallVirtualFromConstructorBase), - [CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived), [GetThis.JniTypeName] = typeof (GetThis), [IAndroidInterface.JniTypeName] = typeof (IAndroidInterface), [IJavaInterface.JniTypeName] = typeof (IJavaInterface), @@ -104,20 +98,13 @@ protected override IEnumerable GetSimpleReferences (Type type) IEnumerable CreateSimpleReferencesEnumerator (Type type) { foreach (var e in TypeMappings) { - if (e.Value == type) { - if (ReplacmentTypes.TryGetValue (e.Key, out var alt)) { - yield return alt; - continue; - } + if (e.Value == type) yield return e.Key; - } } } - public string? RequestedFallbackTypesForSimpleReference; protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) { - RequestedFallbackTypesForSimpleReference = jniSimpleReference; Debug.WriteLine ($"# GetStaticMethodFallbackTypes (jniSimpleReference={jniSimpleReference})"); var slash = jniSimpleReference.LastIndexOf ('/'); @@ -134,52 +121,12 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) }; } - Dictionary ReplacmentTypes = new() { - ["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2", - }; - - string? trackedReplacementType; - int replacementTypeStringLookupCount; - int replacementTypeUtf8LookupCount; - - public void TrackReplacementTypeLookups (string jniSimpleReference) - { - trackedReplacementType = jniSimpleReference; - replacementTypeStringLookupCount = 0; - replacementTypeUtf8LookupCount = 0; - } - - public (int String, int Utf8) GetReplacementTypeLookupCounts () - => (replacementTypeStringLookupCount, replacementTypeUtf8LookupCount); - - protected override string? GetReplacementTypeCore (string jniSimpleReference) - { - if (jniSimpleReference == trackedReplacementType) - Interlocked.Increment (ref replacementTypeStringLookupCount); - return ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) - ? value - : null; - } - - protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) - { - if (jniSimpleReference == trackedReplacementType) - Interlocked.Increment (ref replacementTypeUtf8LookupCount); - replacement = null; - replacementUtf8 = ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) - ? GetUtf8Value (value) - : IntPtr.Zero; - } - Dictionary<(string SourceType, string SourceName, string? SourceSignature), (string? TargetType, string? TargetName, string? TargetSignature, int? ParamCount, bool TurnStatic, ReplacementMethodStorage Storage)> ReplacementMethods = new() { [("java/lang/Object", "remappedToToString", "()Ljava/lang/String;")] = (null, "toString", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), [("java/lang/Object", "remappedToStringWithUtf8Signature", "()Ljava/lang/String;")] = (null, "toString", "()Ljava/lang/String;", null, false, ReplacementMethodStorage.SignatureUtf8), [("java/lang/Object", "remappedToStaticHashCode", null)] = ("net/dot/jni/test/ObjectHelper", "getHashCodeHelper", null, null, true, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8 | ReplacementMethodStorage.SignatureUtf8), [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false, ReplacementMethodStorage.Strings), - // NOTE: key must use *post-renamed* value, not pre-renamed value - // NOTE: SourceSignature lacking return type; "closer in spirit" to what `remapping-config.json` allows - [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), }; protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs deleted file mode 100644 index 2949f14286c..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests -{ - [TestFixture] - public class JniInstanceMethodIDTest : JavaVMFixture - { - // https://code.google.com/p/android/issues/detail?id=65710 - [Test] - public unsafe void CallNonvirtualVoidMethod_WithBaseMethodIDAndDerivedType () - { - using (var b = new JniType ("net/dot/jni/test/CallNonvirtualBase")) - using (var d = new JniType ("net/dot/jni/test/CallNonvirtualDerived")) { - var m = b.GetInstanceMethod ("method", "()V"); - var f = b.GetInstanceField ("methodInvoked", "Z"); - - var c = d.GetConstructor ("()V"); - var g = d.GetInstanceField ("methodInvoked", "Z"); - var o = d.NewObject (c, null); - try { - if (JavaVMFixture.CallNonvirtualVoidMethodSupportsDeclaringClassMismatch) { - JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod (o, d.PeerReference, m); - Assert.IsFalse (JniEnvironment.InstanceFields.GetBooleanField (o, f)); - Assert.IsTrue (JniEnvironment.InstanceFields.GetBooleanField (o, g)); - } else { - JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod (o, d.PeerReference, m); - Assert.IsTrue (JniEnvironment.InstanceFields.GetBooleanField (o, f)); - Assert.IsFalse (JniEnvironment.InstanceFields.GetBooleanField (o, g)); - } - } finally { - JniObjectReference.Dispose (ref o); - } - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index 83ebf2ece31..dd3ed929c22 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -18,62 +18,6 @@ public void Ctor_CanReferenceNonexistentType () JniPeerMembers.Dispose (members); } -#if !ANDROID // Android doesn't allow providing a custom TypeManager - [Test] - [NonParallelizable] - public void HandledReplacementTypeMissDoesNotUseStringFallback () - { - var typeManager = JavaVMFixture.TypeManager; - Assert.IsNotNull (typeManager); - typeManager.TrackReplacementTypeLookups ("java/lang/Double"); - try { - var members = new JniPeerMembers ("java/lang/Double", typeof (MyString)); - JniPeerMembers.Dispose (members); - - var counts = typeManager.GetReplacementTypeLookupCounts (); - Assert.AreEqual (1, counts.Utf8); - Assert.AreEqual (0, counts.String); - } finally { - typeManager.TrackReplacementTypeLookups (""); - } - } -#endif // !ANDROID - -#if !__ANDROID__ - [Test] - public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () - { - Assert.IsNull (GetInstanceMethods (MyString._members.InstanceMethods)); - using (var s = new MyString ("hello!")) { - var registered = GetInstanceMethods (MyString._members.InstanceMethods); - Assert.AreEqual (1, registered.Count); // for the constructor - Assert.AreEqual ("hello!", s.ToString ()); - Assert.AreEqual (1, registered.Count); - } - } - - [Test] - public void ConcurrentFirstUsePublishesSingleInstanceMethodCache () - { - var members = new JniPeerMembers (MyString.JniTypeName, typeof (MyString)); - try { - var methods = members.InstanceMethods; - var constructors = new JniMethodInfo [16]; - - Assert.IsNull (GetInstanceMethods (methods)); - Parallel.For (0, constructors.Length, i => constructors [i] = methods.GetConstructor ("()V")); - - var registered = GetInstanceMethods (methods); - Assert.AreEqual (1, registered.Count); - foreach (var constructor in constructors) - Assert.AreSame (constructors [0], constructor); - Assert.AreSame (registered ["()V"], constructors [0]); - } finally { - JniPeerMembers.Dispose (members); - } - } -#endif // !__ANDROID__ - [Test] public void PeerMemberCachesAreInitiallyNull () { @@ -219,24 +163,9 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () // --- End of managed Java.Lang.NoSuchMethodError stack trace --- // ``` Assert.IsTrue (e.Message.Contains ("doesNotExist", StringComparison.Ordinal)); -#if !ANDROID // Android doesn't allow providing a custom TypeManager - Assert.AreEqual ("java/lang/Runtime", - JavaVMFixture.TypeManager.RequestedFallbackTypesForSimpleReference); -#endif // !ANDROID } } -#if !__ANDROID__ - [Test] - [Category ("NativeAOTIgnore")] - public void ReplacementTypeUsedForMethodLookup () - { - using var o = new RenameClassDerived (); - int r = o.hashCode(); - Assert.AreEqual (33, r); - } -#endif // !__ANDROID__ - [Test] [Category ("NativeAOTIgnore")] public void ReplaceInstanceMethodName () @@ -274,54 +203,6 @@ public void ReplaceInstanceMethodWithStaticMethod () o.remappedToStaticHashCode (); } -#if !__ANDROID__ - // Note: this test looks up a static method from one class, then - // calls `JNIEnv::CallStaticObjectMethod()` passing in a jclass - // for a *different* class. - // - // This appears to work on Desktop JVM. - // - // On Android, this will ABORT the app: - // JNI DETECTED ERROR IN APPLICATION: can't call static int com.xamarin.interop.DesugarAndroidInterface$_CC.getClassName() with class java.lang.Class - // in call to CallStaticObjectMethodA - // - // This *also* aborts on JDK-17 + macOS + arm64: - // FATAL ERROR in native method: Wrong object class or methodID passed to JNI call - // Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) - // V [libjvm.dylib+0x4f718c] jniCheck::validate_call(JavaThread*, _jclass*, _jmethodID*, _jobject*)+0x98 - // V [libjvm.dylib+0x506474] checked_jni_CallStaticObjectMethodA+0x150 - // C [native JNI wrapper] java_interop_jnienv_call_static_object_method_a+0x48 - // C 0x000000010798a8d8 - // C 0x000000010798a540 - // C 0x000000010798fa94 - // C [libcoreclr.dylib+0x2db774] CallDescrWorkerInternal+0x84 - // C [libcoreclr.dylib+0x150db4] CallDescrWorkerWithHandler(CallDescrData*, int)+0x74 - // C [libcoreclr.dylib+0x1f92d0] RuntimeMethodHandle::InvokeMethod(Object*, void**, SignatureNative*, bool)+0x79c - // C 0x0000000104020ce4 - // … - // - // *Fascinating* the differences that can appear between JVM implementations - [Test] - public unsafe void DoesTheJmethodNeedToMatchDeclaringType () - { - if (Environment.GetEnvironmentVariable ("CPUTYPE") is string cpu && cpu == "arm64") { - Assert.Ignore ("nope!"); - } - if (VM.JdkInfo.Version is Version v && v >= new Version (17, 0)) { - // JDK-17 now crashes on this test as well: - // FATAL ERROR in native method: Wrong object class or methodID passed to JNI call - Assert.Ignore ("nope!"); - } - var iface = new JniType ("net/dot/jni/test/AndroidInterface"); - var desugar = new JniType ("net/dot/jni/test/DesugarAndroidInterface$_CC"); - var m = desugar.GetStaticMethod ("getClassName", "()Ljava/lang/String;"); - - var r = JniEnvironment.StaticMethods.CallStaticObjectMethod (iface.PeerReference, m, null); - var s = JniEnvironment.Strings.ToString (ref r, JniObjectReferenceOptions.CopyAndDispose); - Assert.AreEqual ("DesugarAndroidInterface$-CC", s); - } -#endif // !__ANDROID__ - [Test] public void DesugarInterfaceStaticMethod () { @@ -335,27 +216,6 @@ abstract class JavaLangSystemTestObject : JavaObject { internal const string JniTypeName = "java/lang/System"; } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class MyString : JavaObject { - internal const string JniTypeName = "java/lang/String"; - - internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (MyString)); - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - public unsafe MyString (string value) - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - const string id = "(Ljava/lang/String;)V"; - var peer = _members.InstanceMethods.StartGenericCreateInstance (id, GetType (), value); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - _members.InstanceMethods.FinishGenericCreateInstance (id, this, value); - } - } - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] class JavaLangRemappingTestObject : JavaObject { internal const string JniTypeName = "java/lang/Object"; @@ -408,37 +268,6 @@ public static unsafe void doesNotExist () } } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class RenameClassBase : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/RenameClassBase1"; - static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (RenameClassBase)); - - public override JniPeerMembers JniPeerMembers => _members; - - public RenameClassBase () - { - } - - public virtual unsafe int hashCode () - { - const string id = "hashCode.()I"; - return _members.InstanceMethods.InvokeVirtualInt32Method (id, this, null); - } - } - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class RenameClassDerived : RenameClassBase { - internal new const string JniTypeName = "net/dot/jni/test/RenameClassDerived"; - public RenameClassDerived () - { - } - - public override unsafe int hashCode () - { - return base.hashCode (); - } - } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] interface IAndroidInterface : IJavaPeerable { internal const string JniTypeName = "net/dot/jni/test/AndroidInterface"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs deleted file mode 100644 index 8a5ef10a2b8..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - public class JniRuntimeJniTypeManagerTests : JavaVMFixture { - - [Test] - public void ReplacementMethodInfoToStringConvertsUtf8Pointers () - { - var type = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); - var name = Marshal.StringToCoTaskMemUTF8 ("toString"); - var signature = Marshal.StringToCoTaskMemUTF8 ("()Ljava/lang/String;"); - try { - var info = new JniRuntime.ReplacementMethodInfo { - TargetJniTypeUtf8 = type, - TargetJniMethodNameUtf8 = name, - TargetJniMethodSignatureUtf8 = signature, - }; - - var value = info.ToString (); - - Assert.That (value, Does.Contain ("TargetJniTypeUtf8 = \"java/lang/Object\"")); - Assert.That (value, Does.Contain ("TargetJniMethodNameUtf8 = \"toString\"")); - Assert.That (value, Does.Contain ("TargetJniMethodSignatureUtf8 = \"()Ljava/lang/String;\"")); - } finally { - Marshal.ZeroFreeCoTaskMemUTF8 (type); - Marshal.ZeroFreeCoTaskMemUTF8 (name); - Marshal.ZeroFreeCoTaskMemUTF8 (signature); - } - } - - [Test] - public void ReplacementTypeInfoSupportsStringAndUtf8Results () - { - using var stringManager = new StringReplacementTypeManager (); - stringManager.GetReplacementTypeInfo ("java/lang/String", out var stringReplacement, out var stringReplacementUtf8); - Assert.AreEqual ("java/lang/Object", stringReplacement); - Assert.AreEqual (IntPtr.Zero, stringReplacementUtf8); - Assert.AreEqual (1, stringManager.LookupCount); - - var utf8Value = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); - try { - using var utf8Manager = new Utf8ReplacementTypeManager (utf8Value); - utf8Manager.GetReplacementTypeInfo ("java/lang/Double", out var missingReplacement, out var missingReplacementUtf8); - Assert.IsNull (missingReplacement); - Assert.AreEqual (IntPtr.Zero, missingReplacementUtf8); - utf8Manager.GetReplacementTypeInfo ("java/lang/String", out var replacement, out var replacementUtf8); - Assert.IsNull (replacement); - Assert.AreEqual (utf8Value, replacementUtf8); - Assert.AreEqual ("java/lang/Object", utf8Manager.GetReplacementType ("java/lang/String")); - } finally { - Marshal.ZeroFreeCoTaskMemUTF8 (utf8Value); - } - } - -#if !__ANDROID__ - [Test] - [RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")] - public void GetInvokerType () - { - using (var vm = new MyTypeManager ()) { - // Concrete type; no invoker - Assert.IsNull (vm.GetInvokerType (typeof (JavaObject))); - - // Not a bound abstract Java type; no invoker - Assert.IsNull (vm.GetInvokerType (typeof (System.ICloneable))); - - // Bound abstract Java type; has an invoker - Assert.AreSame (typeof (IJavaInterfaceInvoker), vm.GetInvokerType (typeof (IJavaInterface))); - } - } - - [RequiresDynamicCode ("MyTypeManager uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("MyTypeManager uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")] - class MyTypeManager : JniRuntime.ReflectionJniTypeManager { - public MyTypeManager () - { - } - } -#endif // !__ANDROID__ - - class StringReplacementTypeManager : JniRuntime.JniTypeManager { - - public int LookupCount { get; private set; } - - protected override string GetReplacementTypeCore (string jniSimpleReference) - { - LookupCount++; - return jniSimpleReference == "java/lang/String" ? "java/lang/Object" : null; - } - } - - class Utf8ReplacementTypeManager : JniRuntime.JniTypeManager { - - readonly IntPtr replacement; - - public Utf8ReplacementTypeManager (IntPtr replacement) - { - this.replacement = replacement; - } - - protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string replacement, out IntPtr replacementUtf8) - { - replacement = jniSimpleReference == "java/lang/String" ? "ignored/string/value" : null; - replacementUtf8 = jniSimpleReference == "java/lang/String" ? this.replacement : IntPtr.Zero; - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs deleted file mode 100644 index a712105d7be..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Collections.Generic; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - public class JniRuntimeJniValueManagerTests : JavaVMFixture { - - [Test] - public void CreateValue () - { - using (var vm = new MyValueManager ()) - using (var o = new JavaObject ()) { - vm.OnSetRuntime (JniRuntime.CurrentRuntime); - - var r = o.PeerReference; - var x = (IJavaPeerable) vm.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x.Dispose (); - - x = vm.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x.Dispose (); - } - } - - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] - class MyValueManager : JniRuntime.ReflectionJniValueManager { - - public override void WaitForGCBridgeProcessing () - { - } - - public override void CollectPeers () - { - } - - public override void AddPeer (IJavaPeerable reference) - { - } - - public override void RemovePeer (IJavaPeerable reference) - { - } - - public override void FinalizePeer (IJavaPeerable reference) - { - } - - public override List GetSurfacedPeers () - { - return null; - } - - public override IJavaPeerable PeekPeer (JniObjectReference reference) - { - return null; - } - - } - - [Test] - public void GetValue_ReturnsAlias () - { - var local = new JavaObject (); - local.UnregisterFromRuntime (); - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (local.PeerReference)); - // GetObject must always return a value (unless handle is null, etc.). - // However, since we called local.UnregisterFromRuntime(), - // JniRuntime.PeekObject() is null (asserted above), but GetObject() must - // **still** return _something_. - // In this case, it returns an _alias_. - // TODO: "most derived type" alias generation. (Not relevant here, but...) - var p = local.PeerReference; - var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (local, alias); - alias.Dispose (); - local.Dispose (); - } - - [Test] - public void GetValue_ReturnsNullWithNullHandle () - { - var r = new JniObjectReference (); - var o = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsNull (o); - } - - [Test] - public void PeekValue () - { - JniObjectReference lref; - using (var o = new JavaObject ()) { - lref = o.PeerReference.NewLocalRef (); - Assert.AreSame (o, JniRuntime.CurrentRuntime.ValueManager.PeekValue (lref)); - } - // At this point, the Java-side object is kept alive by `lref`, - // but the wrapper instance has been disposed, and thus should - // be unregistered, and thus unfindable. - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (lref)); - JniObjectReference.Dispose (ref lref); - } - - public void PeekValue_BoxedObjects () - { - var vm = JniRuntime.CurrentRuntime.ValueManager; - var marshaler = vm.GetValueMarshaler (); - var ad = AppDomain.CurrentDomain; - - var proxy = marshaler.CreateGenericArgumentState (ad); - Assert.AreSame (ad, vm.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ad, ref proxy); - - var ex = new InvalidOperationException ("boo!"); - proxy = marshaler.CreateGenericArgumentState (ex); - Assert.AreSame (ex, vm.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ex, ref proxy); - } - - [Test] - public void GetValue_ReturnsNullWithInvalidSafeHandle () - { - var invalid = new JniObjectReference (); - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose)); - } - -#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - [Test] - public unsafe void GetValue_FindBestMatchType () - { - using (var t = new JniType (TestType.JniTypeName)) { - var c = t.GetConstructor ("()V"); - var o = t.NewObject (c, null); - using (var w = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) { - Assert.AreEqual (typeof (TestType), w.GetType ()); - Assert.IsTrue (((TestType) w).ExecutedActivationConstructor); - } - } - } -#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs deleted file mode 100644 index b52a53edab2..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs +++ /dev/null @@ -1,531 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - -#if !__ANDROID__ - // Modifies JniRuntime.valueManager instance field; can't be done in parallel - [NonParallelizable] - public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture { - - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - protected abstract Type ValueManagerType { - get; - } - - protected virtual JniRuntime.JniValueManager CreateValueManager () - { - var manager = Activator.CreateInstance (ValueManagerType) as JniRuntime.JniValueManager; - return manager ?? throw new InvalidOperationException ($"Could not create instance of `{ValueManagerType}`!"); - } - -#pragma warning disable CS8618 - JniRuntime.JniValueManager systemManager; - JniRuntime.JniValueManager valueManager; -#pragma warning restore CS8618 - - [SetUp] - public void CreateVM () - { - systemManager = JniRuntime.CurrentRuntime.valueManager!; - valueManager = CreateValueManager (); - valueManager.OnSetRuntime (JniRuntime.CurrentRuntime); - JniRuntime.CurrentRuntime.valueManager = valueManager; - } - - [TearDown] - public void DestroyVM () - { - JniRuntime.CurrentRuntime.valueManager = systemManager; - systemManager = null!; - valueManager?.Dispose (); - valueManager = null!; - } - - [Test] - public void AddPeer () - { - } - - int GetSurfacedPeersCount () - { - return valueManager.GetSurfacedPeers ().Count; - } - - [Test] - public void AddPeer_NoDuplicates () - { - int startPeerCount = GetSurfacedPeersCount (); - using (var v = new MyDisposableObject ()) { - // MyDisposableObject ctor implicitly calls AddPeer(); - Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ()); - valueManager.AddPeer (v); - Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ()); - } - } - - [Test] - public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers () - { - int startPeerCount = GetSurfacedPeersCount (); - - var g = new GetThis (); - var surfaced = valueManager.GetSurfacedPeers (); - Assert.AreEqual (startPeerCount + 1, surfaced.Count); - - var found = false; - foreach (var pr in surfaced) { - if (!pr.SurfacedPeer.TryGetTarget (out var p)) - continue; - if (object.ReferenceEquals (g, p)) { - found = true; - } - } - Assert.IsTrue (found); - - var localRef = g.PeerReference.NewLocalRef (); - g.Dispose (); - Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ()); - Assert.IsNull (valueManager.PeekPeer (localRef)); - JniObjectReference.Dispose (ref localRef); - } - - [Test] - public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers () - { - int startPeerCount = GetSurfacedPeersCount (); - - var g = new GetThis (); - var surfaced = valueManager.GetSurfacedPeers (); - Assert.AreEqual (startPeerCount + 1, surfaced.Count); - - var found = false; - foreach (var pr in surfaced) { - if (!pr.SurfacedPeer.TryGetTarget (out var p)) - continue; - if (object.ReferenceEquals (g, p)) { - found = true; - } - } - Assert.IsTrue (found); - - var localRef = g.PeerReference.NewLocalRef (); - g.Dispose (); - Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ()); - Assert.IsNull (valueManager.PeekPeer (localRef)); - JniObjectReference.Dispose (ref localRef); - } - - // https://github.com/dotnet/android/issues/11101 - [Test] - public void ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefs () - { - // Simulate what TypeManager.Activate does in dotnet/android: - // 1. Create an uninitialized JavaObject - // 2. Set a peer reference (storing the raw handle) - // 3. The constructor chain then calls ConstructPeer multiple times - // - // This should not leak JNI global references. - - using (var original = new MyDisposableObject ()) { - // Get the jobject handle, simulating the IntPtr jobject param in Activate - var handle = original.PeerReference; - - // Create an uninitialized peer and set its reference (simulating Activate) - var peer = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (typeof (MyDisposableObject)); - peer.SetPeerReference (new JniObjectReference (handle.Handle)); - - try { - // Now simulate the constructor chain calling ConstructPeer multiple times - var ref1 = new JniObjectReference (handle.Handle); - valueManager.ConstructPeer (peer, ref ref1, JniObjectReferenceOptions.Copy); - - int grefAfterFirst = JniEnvironment.Runtime.GlobalReferenceCount; - - var ref2 = new JniObjectReference (handle.Handle); - valueManager.ConstructPeer (peer, ref ref2, JniObjectReferenceOptions.Copy); - - int grefAfterSecond = JniEnvironment.Runtime.GlobalReferenceCount; - - // The second ConstructPeer should NOT create an additional global ref - Assert.AreEqual (grefAfterFirst, grefAfterSecond, - "Second ConstructPeer call should not create an additional global ref"); - } finally { - peer.Dispose (); - } - } - } - - - [Test] - public void CollectPeers () - { - // TODO - } - - [Test] - public void CreatePeer_InvalidHandleReturnsNull () - { - var r = new JniObjectReference (); - var o = valueManager.CreatePeer (ref r, JniObjectReferenceOptions.Copy, null); - Assert.IsNull (o); - } - - [Test] - public unsafe void CreatePeer_UsesFallbackType () - { - using var t = new JniType (AnotherJavaInterfaceImpl.JniTypeName); - - var ctor = t.GetConstructor ("()V"); - var lref = t.NewObject (ctor, null); - - using var p = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (IJavaInterface)); - - Assert.IsFalse (lref.IsValid); // .CopyAndDispose disposes - - Assert.IsNotNull (p); - Assert.AreSame (typeof (IJavaInterfaceInvoker), p!.GetType ()); - } - - [Test] - public void CreatePeer_CreatesNewValueUsingActivationConstructor () - { - using var v1 = new AnotherJavaInterfaceImpl (); - var lref = v1.PeerReference.NewLocalRef (); - try { - using var v2 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (AnotherJavaInterfaceImpl)); - Assert.AreNotSame (v1, v2, "CreatePeer() should create new values"); - } - finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void DisposePeerUnlessReferenced_CallsManagedDispose () - { - using var registered = new AnotherJavaInterfaceImpl (); - var lref = registered.PeerReference.NewLocalRef (); - try { - using var alias = valueManager.CreatePeer ( - ref lref, - JniObjectReferenceOptions.Copy, - typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl - ?? throw new InvalidOperationException ("Could not create peer alias."); - - Assert.AreNotSame (registered, alias); - Assert.AreSame (registered, valueManager.PeekPeer (lref)); - - alias.DisposeUnlessReferenced (); - - Assert.IsTrue (alias.DisposeCalled, "The unregistered alias should receive its managed disposal callback."); - Assert.IsFalse (alias.PeerReference.IsValid, "The unregistered alias should release its JNI reference."); - Assert.IsFalse (registered.DisposeCalled, "The registered peer should remain undisposed."); - Assert.IsTrue (registered.PeerReference.IsValid, "The registered peer should retain its JNI reference."); - } finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public unsafe void GetPeer_ReentrantActivation_ReturnsRegisteredPeer () - { - using var t = new JniType (AnotherJavaInterfaceImpl.JniTypeName); - var ctor = t.GetConstructor ("()V"); - var lref = t.NewObject (ctor, null); - AnotherJavaInterfaceImpl? reentrant = null; - var createdPeers = new List (); - try { - Assert.IsNull (valueManager.PeekPeer (lref)); - AnotherJavaInterfaceImpl.PeerCreated = createdPeers.Add; - AnotherJavaInterfaceImpl.BeforeConstruct = reference => { - reentrant = valueManager.GetPeer (reference, typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl; - }; - - var peer = valueManager.GetPeer (lref, typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl - ?? throw new InvalidOperationException ("Could not create peer."); - var registered = valueManager.PeekPeer (lref) - ?? throw new InvalidOperationException ("No peer was registered."); - - Assert.AreSame (reentrant, registered, "The reentrant lookup should register first."); - Assert.AreSame (registered, peer, "The outer lookup should return the registered peer."); - Assert.AreEqual (2, createdPeers.Count, "Both lookups should construct a peer."); - Assert.AreNotSame (createdPeers [0], createdPeers [1]); - foreach (var created in createdPeers) { - Assert.AreEqual (ReferenceEquals (created, registered), created.PeerReference.IsValid, - "Only the returned registered peer should retain a JNI reference."); - Assert.AreEqual (!ReferenceEquals (created, registered), created.DisposeCalled, - "Only the losing alias should receive its managed disposal callback."); - } - } finally { - AnotherJavaInterfaceImpl.BeforeConstruct = null; - AnotherJavaInterfaceImpl.PeerCreated = null; - foreach (var peer in createdPeers) - peer.Dispose (); - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void CreatePeer_ThrowsIfNoActivationConstructorPresent () - { - using var v1 = new GetThis (); - var lref = v1.PeerReference.NewLocalRef (); - var ex = Assert.Throws ( - () => valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (GetThis)), - $"`GetThis` has no activation constructor, so attempting to use it should throw NotSupportedException."); - Assert.IsTrue (lref.IsValid, "lref should still be valid"); - JniObjectReference.Dispose (ref lref); - } - - [Test] - public void CreatePeer_ReplaceableDoesNotReplace () - { - var v = new AnotherJavaInterfaceImpl (); - var lref = v.PeerReference.NewLocalRef (); - v.Dispose (); - - try { - Assert.IsNull (valueManager.PeekPeer (lref), "v.Dispose() should have unregistered the peer."); - var peer1 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.Copy, typeof (AnotherJavaInterfaceImpl)); - Assert.IsTrue ( - peer1!.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable), - $"Expected peer1.JniManagedPeerState to have .Replaceable, but was {peer1.JniManagedPeerState}."); - Assert.AreSame (peer1, valueManager.PeekPeer (lref), - $"Expected peer1==PeekValue(peer1.PeerReference); it's the only one that should exist!"); - - var peer2 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.Copy, typeof (AnotherJavaInterfaceImpl)); - Assert.IsTrue ( - peer2!.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable), - $"Expected peer2.JniManagedPeerState to have .Replaceable, but was {peer2.JniManagedPeerState}."); - Assert.AreNotSame (peer1, peer2, "Expected peer1 and peer2 to be different instances."); - - var peeked = valueManager.PeekPeer (lref); - Assert.AreSame (peer1, peeked, - "Expected peer1 and peeked to be the same instance; " + - $"peeked={RuntimeHelpers.GetHashCode (peeked).ToString ("x")}, " + - $"peer1={RuntimeHelpers.GetHashCode (peer1).ToString ("x")}, " + - $"peer2={RuntimeHelpers.GetHashCode (peer2).ToString ("x")}"); - } finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void CreateValue () - { - using (var o = new JavaObject ()) { - var r = o.PeerReference; - var x = (IJavaPeerable) valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy)!; - Assert.AreNotSame (o, x); - x.Dispose (); - - x = valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x!.Dispose (); - } - } - - [Test] - public void GetValue_ReturnsAlias () - { - var local = new JavaObject (); - local.UnregisterFromRuntime (); - Assert.IsNull (valueManager.PeekValue (local.PeerReference)); - // GetObject must always return a value (unless handle is null, etc.). - // However, since we called local.UnregisterFromRuntime(), - // JniRuntime.PeekObject() is null (asserted above), but GetObject() must - // **still** return _something_. - // In this case, it returns an _alias_. - // TODO: "most derived type" alias generation. (Not relevant here, but...) - var p = local.PeerReference; - var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (local, alias); - alias!.Dispose (); - local.Dispose (); - } - - [Test] - public void GetValue_ReturnsNullWithNullHandle () - { - var r = new JniObjectReference (); - var o = valueManager.GetValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsNull (o); - } - - [Test] - public void GetValue_ReturnsNullWithInvalidSafeHandle () - { - var invalid = new JniObjectReference (); - Assert.IsNull (valueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose)); - } - - [Test] - public unsafe void GetValue_FindBestMatchType () - { -#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - using (var t = new JniType (TestType.JniTypeName)) { - var c = t.GetConstructor ("()V"); - var o = t.NewObject (c, null); - using (var w = valueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) { - Assert.AreEqual (typeof (TestType), w!.GetType ()); - Assert.IsTrue (((TestType) w).ExecutedActivationConstructor); - } - } -#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - } - - [Test] - public void PeekPeer () - { - Assert.IsNull (valueManager.PeekPeer (new JniObjectReference ())); - - using (var v = new MyDisposableObject ()) { - Assert.IsNotNull (valueManager.PeekPeer (v.PeerReference)); - Assert.AreSame (v, valueManager.PeekPeer (v.PeerReference)); - } - } - - [Test] - public void PeekValue () - { - JniObjectReference lref; - using (var o = new JavaObject ()) { - lref = o.PeerReference.NewLocalRef (); - Assert.AreSame (o, valueManager.PeekValue (lref)); - } - // At this point, the Java-side object is kept alive by `lref`, - // but the wrapper instance has been disposed, and thus should - // be unregistered, and thus unfindable. - Assert.IsNull (valueManager.PeekValue (lref)); - JniObjectReference.Dispose (ref lref); - } - - [Test] - public void PeekValue_BoxedObjects () - { - var marshaler = valueManager.GetValueMarshaler (); - var ad = AppDomain.CurrentDomain; - - var proxy = marshaler.CreateGenericArgumentState (ad); - Assert.AreSame (ad, valueManager.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ad, ref proxy); - - var ex = new InvalidOperationException ("boo!"); - proxy = marshaler.CreateGenericArgumentState (ex); - Assert.AreSame (ex, valueManager.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ex, ref proxy); - } - - void AllNestedRegistrationScopeTests () - { - AddPeer (); - AddPeer_NoDuplicates (); - ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers (); - CreateValue (); - GetValue_FindBestMatchType (); - GetValue_ReturnsAlias (); - GetValue_ReturnsNullWithInvalidSafeHandle (); - GetValue_ReturnsNullWithNullHandle (); - PeekPeer (); - PeekValue (); - PeekValue_BoxedObjects (); - } - - string DumpPeers () - { - return DumpPeers (valueManager.GetSurfacedPeers ()); - } - - static string DumpPeers (IEnumerable peers) - { - return string.Join ("," + Environment.NewLine, peers); - } - - - // also test: - // Singleton scenario - // Types w/o "activation" constructors -- need to support checking parent scopes - // nesting of scopes - // Adding an instance already added in a previous scope? - } - - public abstract class JniRuntimeJniValueManagerContract< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - T - > : JniRuntimeJniValueManagerContract - { - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - protected override Type ValueManagerType => typeof (T); - } - - [TestFixture] - public class JniRuntimeJniValueManagerContract_NoGCIntegration : JniRuntimeJniValueManagerContract { - } -#endif // !__ANDROID__ - - // Note: Java side implements JavaInterface, while managed binding DOES NOT. - // This is so that `CreatePeer(…, typeof(IJavaInterface))` tests don't use an existing AnotherJavaInterfaceImpl instance. - // - // This is mostly identical to MyJavaInterfaceImpl; the important difference is that - // it contains an activation constructor, while MyJavaInterfaceImpl does not. - // MyJavaInterfaceImpl can't have one, as that's what provokes the NotSupportedException in the JavaAs() tests. - // - // We want one here so that in "bad" `CreatePeer()` implementations, we'll find this peer and construct it - // before verifying that it satisfies the targetType requirement. - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - public class AnotherJavaInterfaceImpl : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/AnotherJavaInterfaceImpl"; - - internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (AnotherJavaInterfaceImpl)); - - internal static Action? BeforeConstruct; - internal static Action? PeerCreated; - - public bool DisposeCalled { - get; - private set; - } - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - unsafe AnotherJavaInterfaceImpl (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - var callback = BeforeConstruct; - BeforeConstruct = null; - callback?.Invoke (reference); - Construct (ref reference, options); - PeerCreated?.Invoke (this); - } - - public unsafe AnotherJavaInterfaceImpl () - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - const string id = "()V"; - var peer = _members.InstanceMethods.StartCreateInstance (id, GetType (), null); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - _members.InstanceMethods.FinishCreateInstance (id, this, null); - } - - protected override void Dispose (bool disposing) - { - if (disposing) - DisposeCalled = true; - base.Dispose (disposing); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs index d7af269401a..63144cf2fec 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Diagnostics.CodeAnalysis; @@ -11,9 +10,6 @@ namespace Java.InteropTests { -#if !__ANDROID__ - [NonParallelizable] -#endif // !__ANDROID__ [TestFixture] public class JniRuntimeTest : JavaVMFixture { @@ -25,62 +21,12 @@ public void CreateJavaVM () Assert.IsTrue (JniEnvironment.EnvironmentPointer != IntPtr.Zero); } -#if !__ANDROID__ - [Test] - [RequiresDynamicCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not trimming-compatible.")] - public void JDK_OnlySupportsOneVM () - { - try { - var second = new TestJVM (new TestJVMOptions () { - JvmLibraryPath = TestJVM.GetJvmLibraryPath (), - }); - // If we reach here, we're in a JVM that supports > 1 VM - second.Dispose (); - Assert.Ignore (); - } catch (NotSupportedException) { - } catch (Exception e){ - Assert.Fail ("Expected NotSupportedException; got: {0}", e); - } - } - - [Test] - [RequiresDynamicCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not trimming-compatible.")] - public void UseInvocationPointerOnNewThread () - { - var InvocationPointer = JniRuntime.CurrentRuntime.InvocationPointer; - - var t = new Thread (() => { - try { - var second = new TestJVM (new TestJVMOptions () { - InvocationPointer = InvocationPointer, - }); - } - catch (Exception e) { - Assert.Fail ("Expected no exception, got: {0}", e); - } - }); - t.Start (); - t.Join (); - } -#endif // !__ANDROID__ - [Test] public void CreateJavaVMWithNullBuilder () { Assert.Throws (() => new JavaVMWithNullBuilder ()); } -#if !__ANDROID__ - [Test] - public void BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault () - { - var types = JniRuntime.CurrentRuntime.TypeManager.GetTypes (new JniTypeSignature (ManagedPeer.JniTypeName)); - Assert.IsTrue (types.Contains (typeof (ManagedPeer))); - } -#endif // !__ANDROID__ - class JavaVMWithNullBuilder : JniRuntime { public JavaVMWithNullBuilder () : base ((JniRuntime.CreationOptions) null) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs index 3a407b527d1..ff5fb0d66a1 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs @@ -88,18 +88,6 @@ public void GetTypeSignature_Type () AssertGetJniTypeInfoForType (typeof (JavaArray), "[[I", true, 2); AssertGetJniTypeInfoForType (typeof (JavaArray[]), "[[[I", true, 3); -#if !__ANDROID__ - // Re-enable once typemap files contain `JavaObject` subclasses, not just Java.Lang.Object subclasses - // - // Note: dotnet/android@5c23bcda updates Java.Lang.Object to inherit JavaObject; this is not enough, - // as `` only processes assemblies if they reference Mono.Android.dll. - AssertGetJniTypeInfoForType (typeof (GenericHolder), GenericHolder.JniTypeName, false, 0); - - // XAJavaInterop1 Java Callable Wrappers, as used in dotnet/android, may contain - // `Runtime.register("Java.InteropTests.GenericHolder`1, Mono.Android-Tests", …)`, - // which results in a generic type definition. Permit this. - AssertGetJniTypeInfoForType (typeof (GenericHolder<>), GenericHolder.JniTypeName, false, 0); -#endif // !__ANDROID__ } static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeyword, int arrayRank) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs index 97397fc70a6..6ef4b07b487 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using Java.Interop; @@ -19,5 +20,55 @@ public void Constructor () Assert.AreEqual (a.MarshalerType, typeof (DemoValueTypeValueMarshaler)); } } -} + [JniValueMarshaler (typeof (DemoValueTypeValueMarshaler))] + struct DemoValueType { + public int Value { get; } + + public DemoValueType (int value) + { + Value = value; + } + } + + class DemoValueTypeValueMarshaler : JniValueMarshaler { + + JniValueMarshaler Int32Marshaler; + + public override bool IsJniValueType => Int32Marshaler.IsJniValueType; + + public DemoValueTypeValueMarshaler () + { + Int32Marshaler = JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); + } + + public override DemoValueType CreateGenericValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType) + { + var value = Int32Marshaler.CreateGenericValue (ref reference, options, typeof (int)); + return new DemoValueType (value); + } + + public override JniValueMarshalerState CreateGenericArgumentState (DemoValueType value, ParameterAttributes synchronize) + { + return Int32Marshaler.CreateGenericArgumentState (value.Value, synchronize); + } + + public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (DemoValueType value, ParameterAttributes synchronize) + { + return Int32Marshaler.CreateGenericObjectReferenceArgumentState (value.Value, synchronize); + } + + public override void DestroyArgumentState (object value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + Int32Marshaler.DestroyArgumentState ((value as DemoValueType?)?.Value, ref state, synchronize); + } + + public override void DestroyGenericArgumentState (DemoValueType value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + Int32Marshaler.DestroyGenericArgumentState (value.Value, ref state, synchronize); + } + } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs deleted file mode 100644 index 1f704229448..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs +++ /dev/null @@ -1,772 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -using Java.Interop; -using Java.Interop.Expressions; - -using Mono.Linq.Expressions; - -using NUnit.Framework; - -namespace Java.InteropTests { - -#if !__ANDROID__ - public abstract class JniValueMarshalerContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JavaVMFixture - { - - protected abstract T Value {get;} - - protected virtual bool IsJniValueType {get;} - protected virtual bool UsesProxy {get;} - - protected virtual bool Equals (T x, T y) - { - return EqualityComparer.Default.Equals (x, y); - } - - protected virtual void Dispose (T value) - { - } - - protected JniValueMarshaler marshaler; - - protected JniValueMarshalerContractTests (JniValueMarshaler marshaler = null) - { - this.marshaler = marshaler ?? JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); - } - - [Test] - public void TestIsJniValueType () - { - Assert.AreEqual (IsJniValueType, marshaler.IsJniValueType); - } - - [Test] - public void CreateArgumentState () - { - if (IsJniValueType) { - var s = marshaler.CreateArgumentState (Value); - - // Note: Only valid if `Value` isn't 0, so don't do that! - Assert.AreNotEqual (new JniArgumentValue (), s.JniArgumentValue); - - // JniValueMarshaler.CreateArgumentState() for value types only - // fills out the JniArgumentValue value, nothing else. - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.IsNull (s.PeerableValue); - Assert.IsNull (s.Extra); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - else { - var s = marshaler.CreateArgumentState (Value); - - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - } - - [Test] - public void CreateGenericArgumentState () - { - if (IsJniValueType) { - var s = marshaler.CreateGenericArgumentState (Value); - - // Note: Only valid if `Value` isn't 0, so don't do that! - Assert.AreNotEqual (new JniArgumentValue (), s.JniArgumentValue); - - // JniValueMarshaler.CreateArgumentState() for value types only - // fills out the JniArgumentValue value, nothing else. - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.IsNull (s.PeerableValue); - Assert.IsNull (s.Extra); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - else { - var s = marshaler.CreateGenericArgumentState (Value); - - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - } - - [Test] - public void CreateObjectReferenceArgumentState_DefaultValue () - { - var s = marshaler.CreateObjectReferenceArgumentState (default (T)); - - if (IsJniValueType) { - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - } else { - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (), s.JniArgumentValue); - } - Assert.IsNull (s.PeerableValue); - marshaler.DestroyArgumentState (default (T), ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateObjectReferenceArgumentState () - { - var s = marshaler.CreateObjectReferenceArgumentState (Value); - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateGenericObjectReferenceArgumentState_DefaultValue () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (default (T)); - - if (IsJniValueType) { - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - } else { - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (), s.JniArgumentValue); - } - Assert.IsNull (s.PeerableValue); - marshaler.DestroyGenericArgumentState (default (T), ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateGenericObjectReferenceArgumentState () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (Value); - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateValue () - { - var s = marshaler.CreateObjectReferenceArgumentState (Value); - var r = s.ReferenceValue; - var o = marshaler.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsTrue (Equals (Value, (T) o)); - if (!UsesProxy && !typeof(T).IsValueType) - Assert.AreNotSame (Value, o); - marshaler.DestroyArgumentState (Value, ref s); - - Dispose ((T) o); - } - - [Test] - public void CreateGenericValue () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (Value); - var r = s.ReferenceValue; - var o = marshaler.CreateGenericValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsTrue (Equals (Value, o)); - if (!UsesProxy && !typeof(T).IsValueType) - Assert.AreNotSame (Value, o); - marshaler.DestroyGenericArgumentState (Value, ref s); - - Dispose (o); - } - - [Test] - public void DestroyArgumentState () - { - var s = new JniValueMarshalerState (); - marshaler.DestroyArgumentState (null, ref s); - } - - [Test] - public void DestroyGenericArgumentState () - { - var s = new JniValueMarshalerState (); - marshaler.DestroyGenericArgumentState (default (T), ref s); - } - - [Test] - [RequiresUnreferencedCode ("CreateReturnValueFromManagedExpression")] - [RequiresDynamicCode ("CreateReturnValueFromManagedExpression")] - public void CreateReturnValueFromManagedExpression () - { - var runtime = Expression.Variable (typeof (JniRuntime), "__jvm"); - var value = Expression.Variable (typeof (T), "__value"); - var context = new JniValueMarshalerContext (runtime) { - LocalVariables = { - runtime, - value, - }, - }; - var ret = marshaler.CreateReturnValueFromManagedExpression (context, value); - CheckExpression (context, GetExpectedReturnValueFromManagedExpression (runtime.Name, value.Name, ret), ret); - } - - protected virtual string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var valueType = GetTypeName (typeof (T)); - var marshalerType = GetTypeName (marshaler.GetType ()); - return $@"{{ - JniRuntime {jvm}; - {valueType} {value}; - {marshalerType} {value}_marshaler; - JniValueMarshalerState {value}_state; - IntPtr {value}_val; - IntPtr {value}_rtn; - - try - {{ - {value}_marshaler = new {marshalerType}(); - {value}_state = {value}_marshaler.CreateArgumentState((object){value}, ParameterAttributes.None); - {value}_val = {value}_state.ReferenceValue.Handle; - {value}_rtn = References.NewReturnToJniRef({value}_state.ReferenceValue); - return {value}_rtn; - }} - finally - {{ - {value}_marshaler.DestroyArgumentState((object){value}, {value}_state, ParameterAttributes.None); - }} -}}"; - } - - void CheckExpression (JniValueMarshalerContext context, string expected, Expression ret) - { - var body = Expression.Block (context.CreationStatements.Concat (new[]{ ret })); - var cleanup = context.CleanupStatements.Any () - ? (Expression) Expression.Block (context.CleanupStatements.Reverse ()) - : (Expression) Expression.Empty (); - var expr = Expression.TryFinally (body, cleanup); - var block = Expression.Block (context.LocalVariables, expr); - // Convert line endings to Unix style to support building and running on different OS types. - string blockCSharp = block.ToCSharpCode (); - blockCSharp = blockCSharp.Replace ("\r\n", "\n"); - expected = expected.Replace ("\r\n", "\n"); - Console.WriteLine ("# jonp: expected: {0}", GetType ().Name); - Console.WriteLine (blockCSharp); - Assert.AreEqual (expected, blockCSharp); - } - - protected static string GetTypeName (Type type) - { - switch (type.Name) { - case "Boolean": - return "bool"; - case "Byte": - return "byte"; - case "Char": - return "char"; - case "Double": - return "double"; - case "Int16": - return "short"; - case "Int32": - return "int"; - case "Int64": - return "long"; - case "Object": - return "object"; - case "SByte": - return "sbyte"; - case "Single": - return "float"; - } - if (type.IsArray) { - return GetTypeName (type.GetElementType ()) + "[]"; - } - if (!type.IsGenericType) { - return type.Name; - } - var n = new System.Text.StringBuilder (); - n.Append (type.Name); - var b = type.Name.IndexOf ('`'); - n.Remove (b, type.Name.Length - b); - n.Append ("<"); - n.Append (string.Join (", ", type.GenericTypeArguments.Select (tp => GetTypeName (tp)))); - n.Append (">"); - return n.ToString (); - } - } - - [TestFixture] - public class JniValueMarshaler_String_ContractTests : JniValueMarshalerContractTests { - protected override string Value {get {return "value";}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var rname = ((ParameterExpression) ret).Name; - return $@"{{ - JniRuntime {jvm}; - string {value}; - JniObjectReference {value}_ref; - IntPtr {value}_rtn; - - try - {{ - {value}_ref = Strings.NewString({value}); - {value}_rtn = References.NewReturnToJniRef({value}_ref); - return {rname}; - }} - finally - {{ - JniObjectReference.Dispose(__value_ref); - }} -}}"; - } - } - - public abstract class JniValueMarshaler_BuiltinType_ContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JniValueMarshalerContractTests - { - protected override bool IsJniValueType {get {return true;}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var valueType = GetTypeName (typeof (T)); - var rname = ((ParameterExpression) ret).Name; - return $@"{{ - JniRuntime {jvm}; - {valueType} {value}; - - try - {{ - return {rname}; - }} - finally - {{ - default(void); - }} -}}"; - } - } - - [TestFixture] - public class JniValueMarshaler_Boolean_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override bool Value {get {return true;}} - } - - [TestFixture] - public class JniValueMarshaler_SByte_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override sbyte Value {get {return (sbyte) 2;}} - } - - [TestFixture] - public class JniValueMarshaler_Char_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override char Value {get {return '3';}} - } - - [TestFixture] - public class JniValueMarshaler_Int16_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override short Value {get {return (short) 4;}} - } - - [TestFixture] - public class JniValueMarshaler_Int32_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override int Value {get {return 5;}} - } - - [TestFixture] - public class JniValueMarshaler_Int64_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override long Value {get {return 6;}} - } - - [TestFixture] - public class JniValueMarshaler_Single_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override float Value {get {return 7F;}} - } - - [TestFixture] - public class JniValueMarshaler_Double_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override double Value {get {return 8D;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableBoolean_ContractTests : JniValueMarshalerContractTests { - protected override bool? Value {get {return true;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableSByte_ContractTests : JniValueMarshalerContractTests { - protected override sbyte? Value {get {return (sbyte) 2;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableByte_ContractTests : JniValueMarshalerContractTests { - protected override byte? Value {get {return (byte) 200;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableChar_ContractTests : JniValueMarshalerContractTests { - protected override char? Value {get {return '3';}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableInt16_ContractTests : JniValueMarshalerContractTests { - protected override short? Value {get {return (short) 4;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableInt32_ContractTests : JniValueMarshalerContractTests { - protected override int? Value {get {return 5;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableInt64_ContractTests : JniValueMarshalerContractTests { - protected override long? Value {get {return 6;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableSingle_ContractTests : JniValueMarshalerContractTests { - protected override float? Value {get {return 7F;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableDouble_ContractTests : JniValueMarshalerContractTests { - protected override double? Value {get {return 8D;}} - } - - public abstract class JniInt32ArrayValueMarshalerContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JniValueMarshalerContractTests - where T : IEnumerable - { - protected abstract T CreateArray (int[] values); - protected abstract string ValueMarshalerSourceType {get;} - - protected override T Value { - get {return CreateArray (new[]{ 1, 2, 3 });} - } - - protected override bool Equals (T x, T y) - { - return x.SequenceEqual (y); - } - - [Test] - public unsafe void DestroyGenericArgumentState_UpdatesSource () - { - var a = CreateArray (new[]{ 1 }); - var s = marshaler.CreateGenericObjectReferenceArgumentState (a); - fixed (int *p = new[]{3}) - JniEnvironment.Arrays.SetIntArrayRegion (s.ReferenceValue, 0, 1, p); - marshaler.DestroyGenericArgumentState (a, ref s); - Assert.AreEqual (3, a.First ()); - Dispose (a); - } - - protected override void Dispose (T value) - { - var d = value as IDisposable; - if (d != null) { - d.Dispose (); - } - } - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - return $@"{{ - JniRuntime __jvm; - {ValueMarshalerSourceType} __value; - ValueMarshaler __value_marshaler; - JniValueMarshalerState __value_state; - IntPtr __value_val; - IntPtr __value_rtn; - - try - {{ - __value_marshaler = new ValueMarshaler(); - __value_state = __value_marshaler.CreateArgumentState((object)__value, ParameterAttributes.None); - __value_val = __value_state.ReferenceValue.Handle; - __value_rtn = References.NewReturnToJniRef(__value_state.ReferenceValue); - return __value_rtn; - }} - finally - {{ - __value_marshaler.DestroyArgumentState((object)__value, __value_state, ParameterAttributes.None); - }} -}}"; - } - } - - [TestFixture] - public class JniValueMarshaler_Int32Array_ContractTests : JniInt32ArrayValueMarshalerContractTests { - protected override int[] CreateArray (int[] values) {return values;} - - protected override string ValueMarshalerSourceType {get {return "int[]";}} - - [Test] - public unsafe void CreateGenericObjectReferenceArgumentState_OutParameterDoesNotCopy () - { - var a = new[]{ 1 }; - var s = marshaler.CreateGenericObjectReferenceArgumentState (a, ParameterAttributes.Out); - int v; - JniEnvironment.Arrays.GetIntArrayRegion (s.ReferenceValue, 0, 1, &v); - Assert.AreEqual (0, v); - marshaler.DestroyGenericArgumentState (a, ref s); - } - - [Test] - public unsafe void DestroyGenericArgumentState_InParameterDoesNotUpdatesSource () - { - var a = CreateArray (new[]{ 1 }); - var s = marshaler.CreateGenericObjectReferenceArgumentState (a); - fixed (int *p = new[]{3}) - JniEnvironment.Arrays.SetIntArrayRegion (s.ReferenceValue, 0, 1, p); - marshaler.DestroyGenericArgumentState (a, ref s, ParameterAttributes.In); - Assert.AreEqual (1, a.First ()); - Dispose (a); - } - } - - [TestFixture] - public class JniValueMarshaler_ListOfInt32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override IList CreateArray (int[] values) {return values;} - - protected override string ValueMarshalerSourceType {get {return "IList";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaArray_Int32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override JavaArray CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaArray";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaPrimitiveArray_Int32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override JavaPrimitiveArray CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaPrimitiveArray";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaInt32Array_ContractTests : JniInt32ArrayValueMarshalerContractTests { - protected override JavaInt32Array CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaInt32Array";}} - } - - [TestFixture] - public class JniValueMarshaler_object_ContractTests : JniValueMarshalerContractTests { - - readonly object value = new object (); - - protected override object Value {get {return value;}} - protected override bool UsesProxy {get {return true;}} - - [Test] - public unsafe void ProxyObjectMethodsDelegateToManagedValue () - { - var value = new ProxyValue (42); - var equalValue = new ProxyValue (42); - var otherValue = new ProxyValue (43); - var valueState = marshaler.CreateGenericObjectReferenceArgumentState (value); - var equalState = marshaler.CreateGenericObjectReferenceArgumentState (equalValue); - var otherState = marshaler.CreateGenericObjectReferenceArgumentState (otherValue); - - try { - using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject")) { - var equals = proxyType.GetInstanceMethod ("equals", "(Ljava/lang/Object;)Z"); - var hashCode = proxyType.GetInstanceMethod ("hashCode", "()I"); - var toString = proxyType.GetInstanceMethod ("toString", "()Ljava/lang/String;"); - var args = stackalloc JniArgumentValue [1]; - - args [0] = new JniArgumentValue (equalState.ReferenceValue); - Assert.IsTrue (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); - - args [0] = new JniArgumentValue (otherState.ReferenceValue); - Assert.IsFalse (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); - - Assert.AreEqual (value.GetHashCode (), JniEnvironment.InstanceMethods.CallIntMethod (valueState.ReferenceValue, hashCode)); - - var proxyString = JniEnvironment.InstanceMethods.CallObjectMethod (valueState.ReferenceValue, toString); - Assert.AreEqual (value.ToString (), JniEnvironment.Strings.ToString (ref proxyString, JniObjectReferenceOptions.CopyAndDispose)); - } - } finally { - marshaler.DestroyGenericArgumentState (otherValue, ref otherState); - marshaler.DestroyGenericArgumentState (equalValue, ref equalState); - marshaler.DestroyGenericArgumentState (value, ref valueState); - } - } - - [Test] - public void SpecificTypesAreUsed () - { - // As a "GREF optimization", the JniValueMarshaler implementation - // will use a "nested" JniValueMarshaler for the runtime type, - // e.g. a boxed int will use JniValueMarshaler - var s = marshaler.CreateGenericObjectReferenceArgumentState (42); - Assert.AreEqual ("java/lang/Integer", JniEnvironment.Types.GetJniTypeNameFromInstance (s.ReferenceValue)); - marshaler.DestroyGenericArgumentState (42, ref s); - - // Compare to the default proxy behavior... - s = marshaler.CreateGenericObjectReferenceArgumentState (value); - Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JniEnvironment.Types.GetJniTypeNameFromInstance (s.ReferenceValue)); - marshaler.DestroyGenericArgumentState (value, ref s); - } - - sealed class ProxyValue { - - readonly int value; - - public ProxyValue (int value) - { - this.value = value; - } - - public override bool Equals (object obj) - { - return obj is ProxyValue other && value == other.value; - } - - public override int GetHashCode () - { - return value; - } - - public override string ToString () - { - return $"ProxyValue({value})"; - } - } - } - - [TestFixture] - public class JniValueMarshaler_IJavaPeerable_ContractTests : JniValueMarshalerContractTests { - readonly IJavaPeerable value = new JavaObject (); - - protected override IJavaPeerable Value {get {return value;}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var pret = (ParameterExpression) ret; - return $@"{{ - JniRuntime {jvm}; - IJavaPeerable {value}; - JniObjectReference {value}_ref; - IntPtr {value}_rtn; - - try - {{ - if (null == {value}) - {{ - return {value}_ref = new JniObjectReference(); - }} - else - {{ - return {value}_ref = (IJavaPeerable){value}.PeerReference; - }} - {value}_rtn = References.NewReturnToJniRef({value}_ref); - return {pret.Name}; - }} - finally - {{ - default(void); - }} -}}"; - } - } -#endif // !__ANDROID__ - - [JniValueMarshaler (typeof (DemoValueTypeValueMarshaler))] - struct DemoValueType { - public int Value {get;} - - public DemoValueType (int value) - { - Value = value; - } - } - - class DemoValueTypeValueMarshaler : JniValueMarshaler { - - JniValueMarshaler Int32Marshaler; - - public override bool IsJniValueType { - get { - return Int32Marshaler.IsJniValueType; - } - } - - public DemoValueTypeValueMarshaler () - { - Int32Marshaler = JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); - } - - public override DemoValueType CreateGenericValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - Type targetType) - { - var v = Int32Marshaler.CreateGenericValue (ref reference, options, typeof (int)); - return new DemoValueType (v); - } - - public override JniValueMarshalerState CreateGenericArgumentState (DemoValueType value, ParameterAttributes synchronize) - { - return Int32Marshaler.CreateGenericArgumentState (value.Value, synchronize); - } - - public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (DemoValueType value, ParameterAttributes synchronize) - { - return Int32Marshaler.CreateGenericObjectReferenceArgumentState (value.Value, synchronize); - } - - public override void DestroyArgumentState (object value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - Int32Marshaler.DestroyArgumentState ((value as DemoValueType?)?.Value, ref state, synchronize); - } - - public override void DestroyGenericArgumentState (DemoValueType value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - Int32Marshaler.DestroyGenericArgumentState (value.Value, ref state, synchronize); - } - } - -#if !__ANDROID__ - [TestFixture] - class JniValueMarshaler_DemoValueType_ContractTests : JniValueMarshalerContractTests { - - protected override DemoValueType Value {get {return new DemoValueType (42);}} - protected override bool IsJniValueType {get {return true;}} - } -#endif // !__ANDROID__ -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs deleted file mode 100644 index de783870094..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests -{ - [TestFixture] - public class MethodBindingTests : JavaVMFixture - { - // - // https://bugzilla.xamarin.com/show_bug.cgi?id=17630 - // https://bugzilla.xamarin.com/show_bug.cgi?id=17750#c6 - // https://code.google.com/p/android/issues/detail?id=65710 - // - // The scenario: Assume a class hiearchy three-deep: - // CallNonvirtualBase > CallNonvirtualDerived > CallNonvirtualDerived2 - // - // Historically, "normal" binding rules/convention within Xamarin.Android - // is that if a Java class' Derived method overrides a base method - // without changing anything (visibility, return type), then the Derived - // method is not emitted. - // - // Thus consider the java CallNonvirtualDerived.method() method, which - // overrides but doesn't otherwise change CallNonvirtualBase.method(). - // Consequently, it doesn't need to be emitted, saving on IL size. - // - // Which leads us to the problem: what should the body of the "common" - // CallNonvirtualBase.Method() binding do? It needs to do EITHER a - // virtual method dispatch (invocation on non-public types) OR a - // non-virtual method dispatch (e.g. `base.Method()` invocation). - // - // THEN there are concerns about efficiency and not doing too much extra - // work within the binding methods. - // - // Which brings us to THIS scenario: CallNonvirtualBase declares method(). - // CallNonvirtualDerived overrides method(), but the override isn't - // declared within the CallNonvirtualDerived binding. Finally we have - // CallNonvirtualDerived2, which inherits CallNonvirtualDerived and - // doesn't override anything. - // - // Logically then, if we have a CallNonvirtualDerived2 instance and we - // invoke method() upon it, then we SHOULD invoke - // CallNonvirtualDerived.method(). ASSERT that this is the case. - // Failure to do so possibly results in Bug #17630/#65710, or #17750. - // - // Aside: just to get to this point we're throwing in - // JavaObject.JniThresholdType and JavaObject.JniThresholdClass. - // These likely won't survive long. - // - // Aside: in Bug #17630 terms, CallNonvirtualBase is ContextWrapper, - // CallNonvirtualDerived is Activity, and CallNonvirtualDerived2 is - // an Android Callable Wrapper/user subclass - [Test] - public void VirtualMethodBinding () - { - using (var d = new CallNonvirtualDerived ()) { - d.Method (); - Assert.IsTrue (d.MethodInvoked); - } - using (var b = new CallNonvirtualBase ()) { - b.Method (); - Assert.IsTrue (b.MethodInvoked); - } - using (CallNonvirtualBase b = new CallNonvirtualDerived ()) { - b.Method (); - Assert.IsFalse (b.MethodInvoked); - Assert.IsTrue (((CallNonvirtualDerived) b).MethodInvoked); - } - using (var d2 = new CallNonvirtualDerived2 ()) { - d2.Method (); - Assert.IsTrue (((CallNonvirtualDerived) d2).MethodInvoked); - Assert.IsFalse (((CallNonvirtualBase) d2).MethodInvoked); - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java deleted file mode 100644 index 02005c388b3..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.dot.jni.test; - -public class AnotherJavaInterfaceImpl - implements JavaInterface, Cloneable -{ - public String getValue() { - return "Another hello from Java!"; - } - - public Object clone() { - return this; - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java deleted file mode 100644 index f4660d0cb87..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallVirtualFromConstructorBase implements GCUserPeerable { - - static final String assemblyQualifiedName = "Java.InteropTests.CallVirtualFromConstructorBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public CallVirtualFromConstructorBase (int value) { - if (CallVirtualFromConstructorBase.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "System.Int32", - value - ); - } - calledFromConstructor (value); - } - - public void calledFromConstructor (int value) { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java deleted file mode 100644 index 5ae143daf1c..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallVirtualFromConstructorDerived - extends CallVirtualFromConstructorBase - implements GCUserPeerable -{ - static { - net.dot.jni.ManagedPeer.registerNativeMembers ( - CallVirtualFromConstructorDerived.class, - ""); - } - - ArrayList managedReferences = new ArrayList(); - - public CallVirtualFromConstructorDerived (int value) { - super (value); - if (CallVirtualFromConstructorDerived.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - "(I)V", - value - ); - } - } - - public static CallVirtualFromConstructorDerived newInstance (int value) - { - return new CallVirtualFromConstructorDerived (value); - } - - public native void calledFromConstructor (int value); - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java deleted file mode 100644 index d843e43a7c6..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase1 - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase1 () { - System.out.println("RenameClassBase.()"); - if (RenameClassBase1.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public int hashCode () { - System.out.println("RenameClassBase1.hashCode()"); - return 16; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java deleted file mode 100644 index 562e15bff04..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase2 - extends RenameClassBase1 - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase2 () { - System.out.println("RenameClassBase.()"); - if (RenameClassBase2.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public int hashCode () { - System.out.println("RenameClassBase2.hashCode()"); - return 32; - } - - public int myNewHashCode() { - System.out.println("RenameClassBase2.myNewHashCode()"); - return 33; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java deleted file mode 100644 index cfbe173f005..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassDerived - extends RenameClassBase2 // Note: does NOT match C# binding! This is "post Bytecode rewriting" - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassDerived, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassDerived () { - System.out.println("RenameClassDerived.()"); - if (RenameClassDerived.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - // Note: while *at runtime* `RenameClassBase1` is replaced with `RenameClassBase2`, - // Java Callable Wrapper generator doesn't know about that (yet?), and thus the - // *original* method name will be present. - // Not sure if this is actually a problem; perhaps Bytecode rewriting happens *after* - // Java Callable Wrapper generation? - public int hashCode () { - System.out.println("RenameClassDerived.hashCode()"); - return 64; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj index fd25f6e85cb..38c319b894c 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj @@ -27,13 +27,6 @@ - - - - - - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml index 643106c7d20..0b1cea5f4e9 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Remaps.xml @@ -1,7 +1,4 @@ - @@ -29,10 +26,4 @@ source-method-name="remappedToGetRuntime" target-type="java/lang/Runtime" target-method-name="getRuntime" target-method-instance-to-static="false" /> - From b301a725e3becf27b2f577d9028652db6f62cc95 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 16:16:21 +0200 Subject: [PATCH 36/58] Preserve Android test discovery with default type map Root reflection-discovered JcwGen and Mono.Android/Java.Interop fixtures when the type map default resolves at target execution. Always package the Android-safe GetThis fixture from the referenced test library, whose type map property remains unset at project evaluation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.JcwGen-Tests.csproj | 9 +++++++ .../Java.Interop-Tests.targets | 24 +++++++------------ .../Mono.Android.NET-Tests.csproj | 14 ++++++++--- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj index 15d30a5f612..ede4e30982a 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj @@ -36,6 +36,15 @@ + + + + + + + diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 813e443a09a..310adf457e2 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -1,8 +1,7 @@  - <_JavaInteropTestsJarFile>Jars\Mono.Android-Test-classes.jar - <_JavaInteropTestsJarFile Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' ">Jars\Mono.Android-Test-classes-trimmable.jar + <_JavaInteropTestsJarFile>Jars\Mono.Android-Test-classes-trimmable.jar @@ -24,7 +23,7 @@ - - - + Exclude="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\GetThis.java" /> + - - - <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> + + + + + + + + + - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index 691cb182bef..2aedc6b1e76 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -316,25 +316,11 @@ _ResolveAssemblies MSBuild target. <_ExcludedNativeLibraries Condition=" '$(_AndroidRuntime)' == 'CoreCLR' And '$(AndroidIncludeDebugSymbols)' != 'true' " Include="libmscordbi" /> - - - <_KnownRuntimeNativeLibrary Include="libSystem.Globalization.Native.so" /> - <_KnownRuntimeNativeLibrary Include="libSystem.IO.Compression.Native.so" /> - <_KnownRuntimeNativeLibrary Include="libSystem.Native.so" /> - <_KnownRuntimeNativeLibrary Include="libSystem.Security.Cryptography.Native.Android.so" /> - <_KnownRuntimeNativeLibrary Include="libclrjit.so" /> - <_KnownRuntimeNativeLibrary Include="libcorclr.so" /> - - + IncludeDebugSymbols="$(AndroidIncludeDebugSymbols)"> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index 39b01d08114..0592661778b 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -31,10 +31,6 @@ properties that determine build ordering. - - <_NativeRuntimeLinking>_LinkNativeRuntime - - $(_AndroidBuildDependsOn); @@ -81,7 +77,6 @@ properties that determine build ordering. _ReadAndroidManifest; _CompileJava; _CreateApplicationSharedLibraries; - $(_NativeRuntimeLinking); _CompileDex; $(_AfterCompileDex); _CreateBaseApk; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeRuntime.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeRuntime.targets deleted file mode 100644 index 36daf0c2619..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeRuntime.targets +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - <_UnifiedNativeRuntime Include="$(_AndroidApplicationSharedLibraryPath)%(_BuildTargetAbis.Identity)\libnetdroid-unified.so"> - %(_BuildTargetAbis.Identity) - libmonodroid.so - - - - - <_ResolvedNativeArchive Include="@(ResolvedFileToPublish)" Condition=" '%(ResolvedFileToPublish.Extension)' == '.a' " /> - <_ResolvedNativeObjectFile Include="@(ResolvedFileToPublish)" Condition=" '%(ResolvedFileToPublish.Extension)' == '.o' " /> - <_ApplicationSharedLibrary Include="@(_UnifiedNativeRuntime)" /> - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs index f0c800c8358..a25becf0fad 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs @@ -100,10 +100,9 @@ void GenerateCompressedAssemblySources () void Generate (Dictionary>? dict) { - var composer = new CompressedAssembliesNativeAssemblyGenerator (Log, dict) { + var generator = new CompressedAssembliesNativeAssemblyGenerator (Log, dict) { EmitComments = EmitLlvmIrComments, }; - LLVMIR.LlvmIrModule compressedAssemblies = composer.Construct (); foreach (string abi in SupportedAbis) { string baseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"compressed_assemblies.{abi.ToLowerInvariant ()}"); @@ -111,7 +110,7 @@ void Generate (Dictionary (); seen = new HashSet (StringComparer.Ordinal); @@ -146,11 +146,10 @@ public override bool RunTask () string jniInitFuncsLlFilePath = outputFile.ItemSpec; var generator = new NativeAotJniInitNativeAssemblyGenerator (Log, bclInitFunctions, customInitFunctions); - LLVMIR.LlvmIrModule jniInitFuncsModule = generator.Construct (); using var jniInitFuncsWriter = MemoryStreamPool.Shared.CreateStreamWriter (); bool fileFullyWritten = false; try { - generator.Generate (jniInitFuncsModule, targetArch, jniInitFuncsWriter, jniInitFuncsLlFilePath!); + generator.Generate (targetArch, jniInitFuncsWriter, jniInitFuncsLlFilePath!); jniInitFuncsWriter.Flush (); Files.CopyIfStreamChanged (jniInitFuncsWriter.BaseStream, jniInitFuncsLlFilePath!); fileFullyWritten = true; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 8fe4acf9d73..159660a70a6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -227,7 +227,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) var jniRemappingNativeCodeInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal (ProjectSpecificTaskObjectKey (GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfoKey), RegisteredTaskObjectLifetime.Build); Dictionary? runtimeProperties = RuntimePropertiesParser.ParseConfig (ProjectRuntimeConfigFilePath, ProjectRuntimeConfigDevFilePath); - LLVMIR.LlvmIrComposer appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, runtimeProperties, Log) { + var appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, runtimeProperties, Log) { UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload, AndroidPackageName = AndroidPackageName, PackageNamingPolicy = pnp, @@ -243,9 +243,11 @@ static bool ShouldSkipAssembly (ITaskItem assembly) MarshalMethodsEnabled = false, IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (), HaveAssemblyStore = UseAssemblyStore, + EmitComments = EmitLlvmIrComments, }; - LLVMIR.LlvmIrModule appConfigModule = appConfigAsmGen.Construct (); - appConfigAsmGen.EmitComments = EmitLlvmIrComments; + + // Any errors in the input data must be reported before any of the output files is written + appConfigAsmGen.Initialize (); foreach (string abi in SupportedAbis) { string targetAbi = abi.ToLowerInvariant (); @@ -255,7 +257,7 @@ static bool ShouldSkipAssembly (ITaskItem assembly) using var appConfigWriter = MemoryStreamPool.Shared.CreateStreamWriter (); try { - appConfigAsmGen.Generate (appConfigModule, targetArch, appConfigWriter, environmentLlFilePath); + appConfigAsmGen.Generate (targetArch, appConfigWriter, environmentLlFilePath); } catch { throw; } finally { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeRuntimeLinkingSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeRuntimeLinkingSources.cs deleted file mode 100644 index 3c50e45cfc3..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeRuntimeLinkingSources.cs +++ /dev/null @@ -1,113 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks; - -/// -/// Generates P/Invoke preservation code for native runtime linking. -/// -/// -/// The generated LLVM IR (.ll) is compiled with the other application native sources. -/// -public class GenerateNativeRuntimeLinkingSources : AndroidTask -{ - /// - /// Gets the task prefix used for logging and error messages. - /// - public override string TaskPrefix => "GNM"; - - /// - /// Gets or sets the Mono runtime components to include in the build. - /// Used for P/Invoke preservation. - /// - public ITaskItem[] MonoComponents { get; set; } = []; - - /// - /// Gets or sets the output directory for environment files. - /// Generated LLVM IR files are written to this directory. - /// - [Required] - public string EnvironmentOutputDirectory { get; set; } = ""; - - /// - /// Gets or sets the resolved assemblies to process. - /// These assemblies are scanned for P/Invokes when native linking is enabled. - /// - [Required] - public ITaskItem [] ResolvedAssemblies { get; set; } = []; - - /// - /// Gets or sets the list of supported Android ABIs to generate code for. - /// Common values include arm64-v8a, armeabi-v7a, x86_64, and x86. - /// - [Required] - public string [] SupportedAbis { get; set; } = []; - - /// - /// Generates the P/Invoke preservation sources for all supported Android ABIs. - /// - /// - /// true if the task completed successfully; false if errors occurred during processing. - /// - /// - /// Scans framework assemblies for P/Invokes and emits LLVM IR for each ABI. - /// - public override bool RunTask () - { - foreach (var abi in SupportedAbis) - Generate (abi); - - return !Log.HasLoggedErrors; - } - - /// - /// Generates a native LLVM IR P/Invoke preservation source for an ABI. - /// - /// The target Android ABI to generate code for (e.g., "arm64-v8a"). - /// - /// Writes pinvoke_preserve.{abi}.ll. - /// - void Generate (string abi) - { - // Setup target information and file paths - var targetAbi = abi.ToLowerInvariant (); - var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); - var pinvokePreserveLlFilePath = Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}.ll"); - - var pinvokePreserveGen = new PreservePinvokesNativeAssemblyGenerator (Log, targetArch, ScanPInvokes (targetArch), MonoComponents); - LLVMIR.LlvmIrModule pinvokePreserveModule = pinvokePreserveGen.Construct (); - using var pinvokePreserveWriter = MemoryStreamPool.Shared.CreateStreamWriter (); - bool fileFullyWritten = false; - try { - pinvokePreserveGen.Generate (pinvokePreserveModule, targetArch, pinvokePreserveWriter, pinvokePreserveLlFilePath); - pinvokePreserveWriter.Flush (); - Files.CopyIfStreamChanged (pinvokePreserveWriter.BaseStream, pinvokePreserveLlFilePath); - fileFullyWritten = true; - } finally { - if (!fileFullyWritten) { - MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{pinvokePreserveLlFilePath}'", pinvokePreserveWriter.BaseStream); - } - } - } - - List ScanPInvokes (AndroidTargetArch arch) - { - // Generated trimmable type map assemblies are attached only to the first ABI. - var assemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, SupportedAbis, validate: false); - if (!assemblies.TryGetValue (arch, out var archAssemblies)) { - throw new InvalidOperationException ($"No resolved assemblies for architecture '{arch}'."); - } - var frameworkAssemblies = archAssemblies.Values - .Where (assembly => bool.TryParse (assembly.GetMetadata ("FrameworkAssembly"), out bool isFramework) && isFramework) - .ToList (); - using var resolver = MonoAndroidHelper.MakeResolver (Log, arch, archAssemblies, loadDebugSymbols: false); - return new PinvokeScanner (Log).Scan (arch, resolver, frameworkAssemblies); - } - -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GetNativeRuntimeComponents.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GetNativeRuntimeComponents.cs deleted file mode 100644 index 0ad28721dc9..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GetNativeRuntimeComponents.cs +++ /dev/null @@ -1,139 +0,0 @@ -#nullable enable -using System; -using System.IO; -using System.Collections.Generic; - -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Microsoft.Android.Build.Tasks; - -namespace Xamarin.Android.Tasks; - -public class GetNativeRuntimeComponents : AndroidTask -{ - public override string TaskPrefix => "GNRC"; - - public ITaskItem[]? MonoComponents { get; set; } - - [Required] - public ITaskItem[] ResolvedNativeArchives { get; set; } = []; - - [Required] - public ITaskItem[] ResolvedNativeObjectFiles { get; set; } = []; - - [Output] - public ITaskItem[] NativeArchives { get; set; } = []; - - [Output] - public ITaskItem[] RequiredLibraries { get; set; } = []; - - [Output] - public ITaskItem[] LinkStartFiles { get; set; } = []; - - [Output] - public ITaskItem[] LinkEndFiles { get; set; } = []; - - // TODO: more research, for now it seems `--export-dynamic-symbol=name` options generated from - // this array don't work as expected. - [Output] - public ITaskItem[] NativeSymbolsToExport { get; set; } = []; - - public override bool RunTask () - { - var components = new NativeRuntimeComponents (MonoComponents); - var uniqueAbis = new HashSet (StringComparer.OrdinalIgnoreCase); - var archives = new List (); - var symbolsToExport = new List (); - - Log.LogDebugMessage ($"Generating list of native files for linking"); - foreach (NativeRuntimeComponents.Archive archiveItem in components.KnownArchives) { - Log.LogDebugMessage ($" archive '{archiveItem.Name}'"); - if (!archiveItem.Include) { - Log.LogDebugMessage (" will not be included"); - continue; - } - MakeArchiveItem (archiveItem, archives, uniqueAbis); - if (archiveItem.SymbolsToPreserve == null || archiveItem.SymbolsToPreserve.Count == 0) { - continue; - } - - foreach (string symbolName in archiveItem.SymbolsToPreserve) { - MakeLibItem (symbolName, symbolsToExport, uniqueAbis); - } - } - - NativeArchives = archives.ToArray (); - NativeSymbolsToExport = symbolsToExport.ToArray (); - - var items = new List (); - foreach (string lib in components.NativeLibraries) { - MakeLibItem (lib, items, uniqueAbis); - } - RequiredLibraries = items.ToArray (); - - items = new List (); - foreach (string startFile in components.LinkStartFiles) { - MakeFileItem ("_NativeLinkStartFiles", startFile, ResolvedNativeObjectFiles, items, uniqueAbis); - } - LinkStartFiles = items.ToArray (); - - items = new List (); - foreach (string endFile in components.LinkEndFiles) { - MakeFileItem ("_NativeLinkEndFiles", endFile, ResolvedNativeObjectFiles, items, uniqueAbis); - } - LinkEndFiles = items.ToArray (); - - return !Log.HasLoggedErrors; - } - - void MakeLibItem (string libName, List libraries, HashSet uniqueAbis) - { - foreach (string abi in uniqueAbis) { - var item = new TaskItem (libName); - item.SetMetadata (KnownMetadata.Abi, abi); - item.SetMetadata (KnownMetadata.NativeSharedLibrary, "true"); - libraries.Add (item); - } - } - - void MakeFileItem (string msbuildItemName, string fileName, ITaskItem[] inputItems, List outputItems, HashSet uniqueAbis) - { - foreach (ITaskItem item in inputItems) { - string name = Path.GetFileName (item.ItemSpec); - if (MonoAndroidHelper.StringEquals (name, fileName, StringComparison.OrdinalIgnoreCase)) { - outputItems.Add (DoMakeItem (msbuildItemName, item, uniqueAbis)); - } - } - } - - void MakeArchiveItem (NativeRuntimeComponents.Archive archive, List archives, HashSet uniqueAbis) - { - foreach (ITaskItem resolvedArchive in ResolvedNativeArchives) { - string fileName = Path.GetFileName (resolvedArchive.ItemSpec); - if (!MonoAndroidHelper.StringEquals (fileName, archive.Name, StringComparison.OrdinalIgnoreCase)) { - continue; - } - - Log.LogDebugMessage ($" creating msbuild item for archive '{archive.Name}'"); - ITaskItem newItem = DoMakeItem ("_ResolvedNativeArchive", resolvedArchive, uniqueAbis); - newItem.SetMetadata (KnownMetadata.NativeLinkWholeArchive, archive.WholeArchive.ToString ()); - newItem.SetMetadata (KnownMetadata.NativeLinkItemSet, archive.SetName); - if (archive.DontExportSymbols) { - newItem.SetMetadata (KnownMetadata.NativeDontExportSymbols, "true"); - } - archives.Add (newItem); - } - } - - ITaskItem DoMakeItem (string msbuildItemName, ITaskItem sourceItem, HashSet uniqueAbis) - { - var ret = new TaskItem (sourceItem.ItemSpec); - string rid = sourceItem.GetRequiredMetadata (msbuildItemName, KnownMetadata.RuntimeIdentifier, Log) ?? String.Empty; - string abi = MonoAndroidHelper.RidToAbi (rid); - uniqueAbis.Add (abi); - ret.SetMetadata (KnownMetadata.Abi, abi); - ret.SetMetadata (KnownMetadata.RuntimeIdentifier, rid); - - return ret; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeRuntime.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeRuntime.cs deleted file mode 100644 index cc2b3391652..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkNativeRuntime.cs +++ /dev/null @@ -1,161 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; - -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -using Microsoft.Android.Build.Tasks; - -namespace Xamarin.Android.Tasks; - -public class LinkNativeRuntime : AsyncTask -{ - public override string TaskPrefix => "LNR"; - - public ITaskItem[]? MonoComponents { get; set; } - - [Required] - public string AndroidBinUtilsDirectory { get; set; } = ""; - - [Required] - public string IntermediateOutputPath { get; set; } = ""; - - [Required] - public ITaskItem[] LinkLibraries { get; set; } = []; - - [Required] - public ITaskItem[] NativeArchives { get; set; } = []; - - [Required] - public ITaskItem[] NativeObjectFiles { get; set; } = []; - - [Required] - public ITaskItem[] NativeLinkStartFiles { get; set; } = []; - - [Required] - public ITaskItem[] NativeLinkEndFiles { get; set; } = []; - - [Required] - public ITaskItem[] NativeSymbolsToExport { get; set; } = []; - - [Required] - public ITaskItem[] OutputRuntimes { get; set; } = []; - - [Required] - public ITaskItem[] SupportedAbis { get; set; } = []; - - [Required] - public ITaskItem[] RuntimePackLibraryDirectories { get; set; } = []; - - public bool SaveDebugSymbols { get; set; } = true; - public bool StripDebugSymbols { get; set; } = true; - - public override System.Threading.Tasks.Task RunTaskAsync () - { - return this.WhenAll (SupportedAbis, LinkRuntime); - } - - void LinkRuntime (ITaskItem abiItem) - { - string abi = abiItem.ItemSpec; - LogDebugMessage ($"LinkRuntime ({abi})"); - ITaskItem outputRuntime = GetFirstAbiItem (OutputRuntimes, "_UnifiedNativeRuntime", abi); - string soname = Path.GetFileNameWithoutExtension (outputRuntime.ItemSpec); - if (soname.StartsWith ("lib", StringComparison.OrdinalIgnoreCase)) { - soname = soname.Substring (3); - } - - var linker = new NativeLinker (Log, abi, soname, AndroidBinUtilsDirectory, IntermediateOutputPath, RuntimePackLibraryDirectories, CancellationToken, Cancel) { - StripDebugSymbols = StripDebugSymbols, - SaveDebugSymbols = SaveDebugSymbols, - UseNdkLibraries = false, - UseSymbolic = true, - }; - - List items = OrganizeCommandLineItemsCLR (abi); - linker.Link ( - outputRuntime, - items, - GetAbiItems (NativeLinkStartFiles, "_NativeLinkStartFiles", abi), - GetAbiItems (NativeLinkEndFiles, "_NativeLinkEndFiles", abi), - GetAbiItems (NativeSymbolsToExport, "_NativeSymbolsToExport", abi) - ); - } - - // Puts object files, static archives in the correct order. This is a bit clumsy, but unfortunately necessary - List OrganizeCommandLineItemsCLR (string abi) - { - // Code farther down the method does NOT check whether a set is present, it assumes that. This is on purpose, to - // let the exception be thrown should a required (and assumed to be present) set be missing. - var sets = new Dictionary> (StringComparer.Ordinal); - foreach (ITaskItem item in GetAbiItems (NativeArchives, "_SelectedNativeArchive", abi)) { - string setName = item.GetRequiredMetadata ("_SelectedNativeArchive", KnownMetadata.NativeLinkItemSet, Log) ?? String.Empty; - if (!sets.TryGetValue (setName, out List? items)) { - items = new List (); - sets.Add (setName, items); - } - - items.Add (item); - } - - var ret = new List (); - - // First go our own object files... - ret.AddRange (GetAbiItems (NativeObjectFiles, "_NativeAssemblyTarget", abi)); - - // ...then go our runtime archives... - ret.AddRange (sets[NativeRuntimeComponents.KnownSets.XamarinAndroidRuntime]); - - // ...followed by CoreCLR components... - ret.AddRange (sets[NativeRuntimeComponents.KnownSets.CoreClrRuntime]); - - // ...and after them the BCL PAL libraries... - ret.AddRange (sets[NativeRuntimeComponents.KnownSets.BCL]); - - // ...and then the C runtime and compiler support libraries - var systemLibs = new Dictionary (StringComparer.Ordinal); - foreach (ITaskItem item in GetAbiItems (LinkLibraries, "_RequiredLinkLibraries", abi)) { - systemLibs.Add (Path.GetFileName (item.ItemSpec), item); - } - - ret.Add (systemLibs["log"]); - ret.AddRange (sets[NativeRuntimeComponents.KnownSets.CompilerRuntime]); - ret.Add (systemLibs["z"]); - ret.Add (systemLibs["m"]); - ret.Add (systemLibs["dl"]); - ret.Add (systemLibs["c"]); - - return ret; - } - - List GetAbiItems (ITaskItem[] source, string itemName, string abi) - { - var ret = new List (); - - foreach (ITaskItem item in source) { - if (AbiMatches (abi, item, itemName)) { - ret.Add (item); - } - } - - return ret; - } - - ITaskItem GetFirstAbiItem (ITaskItem[] source, string itemName, string abi) - { - foreach (ITaskItem item in source) { - if (AbiMatches (abi, item, itemName)) { - return item; - } - } - - throw new InvalidOperationException ($"Internal error: item '{itemName}' for ABI '{abi}' not found"); - } - - bool AbiMatches (string abi, ITaskItem item, string itemName) - { - return MonoAndroidHelper.StringEquals (abi, item.GetRequiredMetadata (itemName, "Abi", Log), StringComparison.OrdinalIgnoreCase); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs b/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs index 023dc507fbc..4545d269d61 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/PrepareAbiItems.cs @@ -15,7 +15,6 @@ public class PrepareAbiItems : AndroidTask const string EnvBase = "environment"; const string CompressedAssembliesBase = "compressed_assemblies"; const string JniRemappingBase = "jni_remap"; - const string PinvokePreserveBase = "pinvoke_preserve"; const string JniInitFuncsBase = "jni_init_funcs"; public override string TaskPrefix => "PAI"; @@ -50,8 +49,6 @@ public override bool RunTask () baseName = CompressedAssembliesBase; } else if (MonoAndroidHelper.StringEquals ("jniremap", Mode, StringComparison.OrdinalIgnoreCase)) { baseName = JniRemappingBase; - } else if (MonoAndroidHelper.StringEquals ("runtime_linking", Mode, StringComparison.OrdinalIgnoreCase)) { - baseName = PinvokePreserveBase; } else if (MonoAndroidHelper.StringEquals ("jni_init", Mode, StringComparison.OrdinalIgnoreCase)) { baseName = JniInitFuncsBase; } else { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessNativeLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessNativeLibraries.cs index bba7568715e..d05abf25c7c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ProcessNativeLibraries.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ProcessNativeLibraries.cs @@ -23,11 +23,6 @@ public class ProcessNativeLibraries : AndroidTask "libxamarin-debug-app-helper", }; - static readonly HashSet RuntimeNativeLibraryNames = new (StringComparer.OrdinalIgnoreCase); - - [Required] - public ITaskItem[] KnownRuntimeNativeLibraries { get; set; } = []; - /// /// Assumed to be .so files only /// @@ -36,7 +31,6 @@ public class ProcessNativeLibraries : AndroidTask public string []? ExcludedLibraries { get; set; } public bool IncludeDebugSymbols { get; set; } - public bool NativeRuntimeLinking { get; set; } [Output] public ITaskItem []? OutputLibraries { get; set; } @@ -53,10 +47,6 @@ public override bool RunTask () } } - foreach (ITaskItem lib in KnownRuntimeNativeLibraries) { - RuntimeNativeLibraryNames.Add (Path.GetFileName (lib.ItemSpec)); - } - var output = new List (InputLibraries.Length); foreach (var library in InputLibraries) { @@ -80,11 +70,6 @@ public override bool RunTask () } if (fileName.StartsWith ("libmono-android", StringComparison.Ordinal) || fileName.StartsWith ("libnet-android", StringComparison.Ordinal)) { - if (NativeRuntimeLinking) { - // We don't need the precompiled runtime, it will be linked during application build - continue; - } - if (fileName.EndsWith (".debug", StringComparison.Ordinal)) { if (!IncludeDebugSymbols) continue; @@ -106,32 +91,12 @@ public override bool RunTask () } } - if (!IgnoreLibraryWhenLinkingRuntime (library)) { - output.Add (library); - } else { - Log.LogDebugMessage ($"Ignoring '{library.ItemSpec}'"); - } + output.Add (library); } OutputLibraries = output.ToArray (); return !Log.HasLoggedErrors; } - - bool IgnoreLibraryWhenLinkingRuntime (ITaskItem libItem) - { - if (!NativeRuntimeLinking) { - return false; - } - - // We ignore all the shared libraries coming from the runtime packages, as they are all linked into our runtime and - // need not be packaged. - if (MonoAndroidHelper.IsFromAKnownRuntimePack (libItem)) { - return true; - } - - // Should `NuGetPackageId` be empty, we check the libs by name, as the last resort. - return RuntimeNativeLibraryNames.Contains (Path.GetFileName (libItem.ItemSpec)); - } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/NativeAotBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/NativeAotBuildTests.cs index fbd55a2a148..e895c0c8267 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/NativeAotBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/NativeAotBuildTests.cs @@ -42,7 +42,6 @@ public void BuildCoreClrWithPrebuiltRuntime (string abi) using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), $"CoreCLR app build should succeed for {abi} with the prebuilt runtime."); - builder.Output.AssertTargetIsSkipped ("_LinkNativeRuntime", defaultIfNotUsed: true); } [TestCase ("armeabi-v7a")] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCompressedAssembliesNativeSourceFilesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCompressedAssembliesNativeSourceFilesTests.cs new file mode 100644 index 00000000000..ef74cc4360d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateCompressedAssembliesNativeSourceFilesTests.cs @@ -0,0 +1,37 @@ +#nullable enable +using System.IO; + +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks; + +[TestFixture] +public class GenerateCompressedAssembliesNativeSourceFilesTests : BaseTest +{ + [Test] + public void EmitsEmptyDescriptorsForAllAbis () + { + string outputDirectory = Path.Combine (Root, "temp", nameof (EmitsEmptyDescriptorsForAllAbis)); + var task = new GenerateCompressedAssembliesNativeSourceFiles { + BuildEngine = new MockBuildEngine (TestContext.Out), + EnvironmentOutputDirectory = outputDirectory, + SupportedAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"], + ProjectFullPath = Path.Combine (Root, "test.csproj"), + Debug = true, + EnableCompression = false, + }; + + Assert.IsTrue (task.Execute (), "GenerateCompressedAssembliesNativeSourceFiles should succeed."); + + foreach (string abi in task.SupportedAbis) { + string source = File.ReadAllText (Path.Combine (outputDirectory, $"compressed_assemblies.{abi}.ll")); + Assert.That (source, Does.Contain ("%struct.CompressedAssemblyDescriptor = type"), abi); + Assert.That (source, Does.Contain ("@compressed_assembly_count = "), abi); + Assert.That (source, Does.Contain ("@compressed_assembly_descriptors = "), abi); + Assert.That (source, Does.Contain ("@uncompressed_assemblies_data_size = "), abi); + Assert.That (source, Does.Contain ("@uncompressed_assemblies_data_buffer = "), abi); + Assert.That (source, Does.Contain ("zeroinitializer"), abi); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs new file mode 100644 index 00000000000..9485db029cb --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateJniRemappingNativeCodeTests.cs @@ -0,0 +1,56 @@ +#nullable enable +using System.IO; + +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests.Tasks; + +[TestFixture] +public class GenerateJniRemappingNativeCodeTests : BaseTest +{ + [TestCase (false)] + [TestCase (true)] + public void EmitsRemappingForAllAbis (bool generateEmptyCode) + { + string outputDirectory = Path.Combine (Root, "temp", $"{nameof (EmitsRemappingForAllAbis)}-{generateEmptyCode}"); + Directory.CreateDirectory (outputDirectory); + + string remappingXml = Path.Combine (outputDirectory, "remapping.xml"); + if (!generateEmptyCode) { + File.WriteAllText (remappingXml, """ + + + + + """); + } + + var task = new GenerateJniRemappingNativeCode { + BuildEngine = new MockBuildEngine (TestContext.Out), + OutputDirectory = outputDirectory, + SupportedAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"], + GenerateEmptyCode = generateEmptyCode, + RemappingXmlFilePath = generateEmptyCode ? null : new TaskItem (remappingXml), + }; + + Assert.IsTrue (task.Execute (), "GenerateJniRemappingNativeCode should succeed."); + + foreach (string abi in task.SupportedAbis) { + string source = File.ReadAllText (Path.Combine (outputDirectory, $"jni_remap.{abi}.ll")); + Assert.That (source, Does.Contain ("@jni_remapping_type_replacements = "), abi); + Assert.That (source, Does.Contain ("@jni_remapping_method_replacement_index = "), abi); + Assert.That (source, Does.Contain ("%struct.JniRemappingIndexTypeEntry = type"), abi); + if (generateEmptyCode) { + Assert.That (source, Does.Not.Contain ("@mm_a_B = "), abi); + } else { + Assert.That (source, Does.Contain ("@mm_a_B = "), abi); + Assert.That (source, Does.Contain ("c\"a/B\\00\""), abi); + Assert.That (source, Does.Contain ("c\"()V\\00\""), abi); + Assert.That (source, Does.Contain ("c\"c/D\\22\\5Cpath\\C3\\A9\\00\""), abi); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs index 1e11f0e39df..30f2bb79ae4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs @@ -11,6 +11,49 @@ namespace Xamarin.Android.Build.Tests.Tasks; [TestFixture] public class GenerateNativeApplicationConfigSourcesTests : BaseTest { + [TestCase (false)] + [TestCase (true)] + public void EmitsApplicationConfigForAllAbis (bool emitComments) + { + string outputRoot = Path.Combine (Root, "temp", $"{nameof (EmitsApplicationConfigForAllAbis)}-{emitComments}"); + string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll"); + FileAssert.Exists (monoAndroidPath); + + var task = new GenerateNativeApplicationConfigSources { + BuildEngine = new MockBuildEngine (TestContext.Out), + ResolvedAssemblies = [new TaskItem (monoAndroidPath)], + EnvironmentOutputDirectory = Path.Combine (outputRoot, "android"), + SupportedAbis = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"], + AndroidPackageName = "com.microsoft.android.llvmassemblytest", + EnablePreloadAssembliesDefault = false, + AndroidRuntime = "CoreCLR", + UseAssemblyStore = true, + EmitLlvmIrComments = emitComments, + }; + + Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed."); + + foreach (var (abi, triple) in new [] { + ("armeabi-v7a", "armv7-unknown-linux-android21"), + ("arm64-v8a", "aarch64-unknown-linux-android21"), + ("x86", "i686-unknown-linux-android21"), + ("x86_64", "x86_64-unknown-linux-android21"), + }) { + string fileName = $"environment.{abi}.ll"; + string source = File.ReadAllText (Path.Combine (outputRoot, "android", fileName)); + Assert.That (source, Does.Contain ($"source_filename = \"{fileName}\""), abi); + Assert.That (source, Does.Contain ($"target triple = \"{triple}\""), abi); + Assert.That (source, Does.Contain ("%struct.ApplicationConfig = type"), abi); + Assert.That (source, Does.Contain ("@application_config = "), abi); + Assert.That (source, Does.Contain ("@app_environment_variables = "), abi); + Assert.That (source, Does.Contain ("@app_system_properties = "), abi); + Assert.That (source, Does.Contain ("@assembly_store = "), abi); + Assert.That (source, Does.Contain ("@dso_cache = "), abi); + Assert.That (source, Does.Contain ("!llvm.module.flags = "), abi); + Assert.That (source, emitComments ? Does.Contain ("; Application environment variables") : Does.Not.Contain ("; Application environment variables"), abi); + } + } + [TestCase (false)] [TestCase (true)] public void HaveAssemblyStoreIsEmittedForCoreCLR (bool haveAssemblyStore) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs deleted file mode 100644 index 9fdd6e03778..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using Microsoft.Build.Utilities; -using NUnit.Framework; -using Xamarin.Android.Tasks; -using Xamarin.Android.Tasks.LLVMIR; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Build.Tests.Tasks -{ - [TestFixture] - public class LlvmIrGeneratorTests - { - /// - /// Regression test for https://github.com/dotnet/android/issues/10679 - /// - /// When a managed method parameter has a whitespace-only name, the LLVM IR - /// generator would produce invalid IR like: - /// define void @test(ptr noundef % ) - /// - /// The fix ensures whitespace-only names are normalized to null before - /// creating LlvmIrFunctionParameter instances, so that LlvmIrFunction - /// assigns them valid numeric names (e.g., %0, %1). - /// - [Test] - [TestCase (null, Description = "Null parameter name")] - [TestCase ("", Description = "Empty parameter name")] - [TestCase (" ", Description = "Space-only parameter name")] - [TestCase (" ", Description = "Multiple spaces parameter name")] - [TestCase ("\t", Description = "Tab-only parameter name")] - public void FunctionParameterWithInvalidName_GetsNumericName (string? paramName) - { - var parameters = new List { - new LlvmIrFunctionParameter (typeof (IntPtr), "env"), - new LlvmIrFunctionParameter (typeof (IntPtr), "klass"), - new LlvmIrFunctionParameter (typeof (IntPtr), paramName), - }; - - var func = new LlvmIrFunction ("test_function", typeof (void), parameters); - - // The third parameter should have been assigned a numeric name - var thirdParam = func.Signature.Parameters[2]; - Assert.IsNotNull (thirdParam.Name, "Parameter name should not be null after LlvmIrFunction construction"); - Assert.IsNotEmpty (thirdParam.Name, "Parameter name should not be empty after LlvmIrFunction construction"); - Assert.That (thirdParam.Name.Trim (), Is.Not.Empty, "Parameter name should not be whitespace-only after LlvmIrFunction construction"); - } - - /// - /// Verifies that the LLVM IR generator produces valid function signatures - /// when parameters have whitespace-only names that would have caused - /// invalid IR like "ptr noundef % )" before the fix. - /// - [Test] - [TestCase (" ", Description = "Space-only parameter name")] - [TestCase ("\t", Description = "Tab-only parameter name")] - public void GeneratedIR_FunctionWithWhitespaceParameterName_ProducesValidOutput (string paramName) - { - var parameters = new List { - new LlvmIrFunctionParameter (typeof (IntPtr), "env"), - new LlvmIrFunctionParameter (typeof (IntPtr), "klass"), - new LlvmIrFunctionParameter (typeof (IntPtr), paramName), - }; - - var func = new LlvmIrFunction ("test_function", typeof (void), parameters); - - var log = new TaskLoggingHelper (new MockBuildEngine (TestContext.Out, [], [], []), "test"); - var module = new LlvmIrModule (new LlvmIrTypeCache (), log); - func.Body.Ret (typeof (void)); - module.Add (func); - module.AfterConstruction (); - - var generator = LlvmIrGenerator.Create (AndroidTargetArch.Arm64, "test.ll"); - using var writer = new StringWriter (); - generator.Generate (writer, module); - - string output = writer.ToString (); - // The output should contain valid parameter declarations - no whitespace-only names after % - Assert.That (output, Does.Not.Contain ("% )"), "Generated LLVM IR should not contain 'ptr noundef % )' pattern"); - Assert.That (output, Does.Not.Contain ("%\t)"), "Generated LLVM IR should not contain 'ptr noundef %\\t)' pattern"); - Assert.That (output, Does.Contain ("@test_function"), "Generated LLVM IR should contain the function name"); - } - - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs new file mode 100644 index 00000000000..705ca785eb1 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs @@ -0,0 +1,72 @@ +#nullable enable +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Build.Tasks; +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Build.Tests.Tasks; + +[TestFixture] +public class LlvmIrWriterTests +{ + [Test] + public void QuotesUtf8AndReservedBytes () + { + byte[] bytes = Encoding.UTF8.GetBytes ("c/D\"\\path\u00e9"); + + Assert.That (LlvmIrWriter.QuoteBytes (bytes, nullTerminated: true), Is.EqualTo ("c\"c/D\\22\\5Cpath\\C3\\A9\\00\"")); + Assert.That (LlvmIrWriter.QuoteBytes (bytes, nullTerminated: false), Is.EqualTo ("c\"c/D\\22\\5Cpath\\C3\\A9\"")); + } + + [Test] + public void StringBlobDeduplicatesStringsByOffset () + { + var blob = new LlvmIrStringBlob (); + + Assert.That (blob.Add ("a"), Is.EqualTo (0)); + Assert.That (blob.Add ("\u00e9"), Is.EqualTo (2)); + Assert.That (blob.Add ("a"), Is.EqualTo (0)); + Assert.That (blob.Size, Is.EqualTo (5)); + Assert.That (blob.Value (), Is.EqualTo ("c\"a\\00\\C3\\A9\\00\"")); + } + + [Test] + public void CommentsCannotInjectLinesIntoIr () + { + using var output = new StringWriter (); + var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: true); + + Assert.That (writer.Comment (" first\nsecond"), Does.Not.Contain ("\n")); + Assert.That (writer.TrailingComment (" first\rsecond"), Does.Not.Contain ("\r")); + writer.WriteCommentLine (" first\nsecond"); + Assert.That (output.ToString ().Split ('\n'), Has.Length.EqualTo (2)); + } + + [Test] + public void NativeAotJniInitDeduplicatesHandlers () + { + var log = new TaskLoggingHelper (new MockBuildEngine (TestContext.Out), "test"); + var generator = new NativeAotJniInitNativeAssemblyGenerator ( + log, + ["AndroidCryptoNative_InitLibraryOnLoad"], + ["AndroidCryptoNative_InitLibraryOnLoad", "My_OnLoad"] + ); + + foreach (AndroidTargetArch arch in new [] { AndroidTargetArch.Arm, AndroidTargetArch.Arm64, AndroidTargetArch.X86, AndroidTargetArch.X86_64 }) { + using var writer = new StringWriter (); + generator.Generate (arch, writer, "jni_init_funcs.ll"); + string output = writer.ToString (); + + Assert.That (output, Does.Contain ("@__jni_on_load_handler_count = dso_local local_unnamed_addr constant i32 2"), arch.ToString ()); + Assert.That (output, Does.Contain ("@__jni_on_load_handlers = dso_local local_unnamed_addr constant [2 x ptr]"), arch.ToString ()); + Assert.That (output, Does.Contain ("@__jni_on_load_handler_names = dso_local local_unnamed_addr constant [2 x ptr]"), arch.ToString ()); + Assert.That (Regex.Matches (output, "declare void @AndroidCryptoNative_InitLibraryOnLoad\\(\\)").Count, Is.EqualTo (1), arch.ToString ()); + Assert.That (Regex.Matches (output, "declare void @My_OnLoad\\(\\)").Count, Is.EqualTo (1), arch.ToString ()); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index dc3128a68da..ba548184277 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -935,9 +935,8 @@ static byte ConvertFieldToByte (string fieldName, string llvmAssemblerEnvFile, s return fv; } - // These fields are always uint32_t/uint64_t and the generator (LlvmIrGenerator/MonoAndroidHelper.CultureInvariantToString) - // writes them via uint/ulong.ToString(), which is always an unsigned decimal representation - there's no signed form to - // account for here, so parse directly as unsigned. + // These fields are always uint32_t/uint64_t and LlvmIrWriter.Number writes unsigned decimal values, + // so parse them directly as unsigned. static bool TryParseInteger (string value, out uint fv) { if (value.StartsWith ("0x", StringComparison.Ordinal)) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs new file mode 100644 index 00000000000..bd9c32791ab --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs @@ -0,0 +1,79 @@ +#nullable enable +using System; +using System.Collections.Generic; + +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// Name:value pairs (environment variables or system properties) stored in an array of +/// AppEnvironmentVariable structures, which refer to NUL-terminated strings in a +/// separate character array. +/// +sealed class AppEnvironmentVariableTable +{ + // Must be identical to the AppEnvironmentVariable structure in src/native/clr/include/xamarin-app.hh + const ulong StructureSize = 8; + const ulong StructureAlignment = 4; + + readonly List<(string Name, string Value, int NameIndex, int ValueIndex)> entries = new (); + readonly LlvmIrStringBlob contents = new (); + + public int Count => entries.Count; + + public AppEnvironmentVariableTable (TaskLoggingHelper log, IDictionary? variables) + { + if (variables == null) { + return; + } + + foreach (var kvp in variables) { + string name = kvp.Key.Trim (); + if (name.Length == 0) { + log.LogDebugMessage ($"Not adding environment variable without a name. Value: '{kvp.Value}'"); + continue; + } + + int nameIndex = contents.Add (name); + int valueIndex = contents.Add (kvp.Value); + entries.Add ((name, kvp.Value, nameIndex, valueIndex)); + } + } + + public static void WriteDeclaration (LlvmIrWriter w) + { + w.Write ($$""" + + %struct.AppEnvironmentVariable = type { + i32, {{w.Comment (" uint32_t name_index")}} + i32 {{w.Comment (" uint32_t value_index")}} + } + + """); + } + + public void Write (LlvmIrWriter w, string arrayName, string contentsName, string comment) + { + var elements = new List (entries.Count); + foreach (var e in entries) { + elements.Add ($$""" + %struct.AppEnvironmentVariable { + i32 {{LlvmIrWriter.Number (e.NameIndex)}}, {{w.Comment ($" '{e.Name}'")}} + i32 {{LlvmIrWriter.Number (e.ValueIndex)}}{{w.Comment ($" '{e.Value}'")}} + } + """); + } + + w.WriteGlobal ( + arrayName, + LlvmIrWriter.GlobalConstant, + $"[{LlvmIrWriter.Number (elements.Count)} x %struct.AppEnvironmentVariable]", + w.ArrayValue (elements, LlvmIrWriter.IndexComment), + w.GetAggregateAlignment (StructureAlignment, (ulong)elements.Count * StructureSize), + comment + ); + contents.Write (w, contentsName); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs index ec788e1f126..c2b64601ba0 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfig.cs @@ -4,7 +4,8 @@ namespace Xamarin.Android.Tasks; // Declaration order of fields and their types must correspond *exactly* to that in -// src/native/clr/xamarin-app-stub/xamarin-app.hh ApplicationConfig structure +// src/native/clr/include/xamarin-app.hh ApplicationConfig structure, as well as to the +// %struct.ApplicationConfig LLVM IR declaration written by ApplicationConfigNativeAssemblyGenerator // // Type mappings: // @@ -21,8 +22,7 @@ namespace Xamarin.Android.Tasks; // int64_t | long // char* | string // -// Names should be the same as in the above struct, but it's not a requirement -// (they will be used only to generate comments in the native code) +// Names should be the same as in the above struct sealed class ApplicationConfig { public bool uses_assembly_preload; @@ -37,13 +37,8 @@ sealed class ApplicationConfig public uint number_of_dso_cache_entries; public uint number_of_shared_libraries; - [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] public uint android_runtime_jnienv_class_token; - - [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] public uint jnienv_initialize_method_token; - - [NativeAssembler (NumberFormat = LLVMIR.LlvmIrVariableNumberFormat.Hexadecimal)] public uint jni_remapping_replacement_type_count; public uint jni_remapping_replacement_method_index_entry_count; public string android_package_name = String.Empty; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs index 7bf0427a1bb..d2bf8dcc9e4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs @@ -8,11 +8,11 @@ using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -using Xamarin.Android.Tasks.LLVMIR; +using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; -class ApplicationConfigNativeAssemblyGenerator : LlvmIrComposer +class ApplicationConfigNativeAssemblyGenerator { // From host_runtime_contract.h in dotnet/runtime const string HOST_PROPERTY_RUNTIME_CONTRACT = "HOST_RUNTIME_CONTRACT"; @@ -21,142 +21,47 @@ class ApplicationConfigNativeAssemblyGenerator : LlvmIrComposer const string HOST_PROPERTY_RUNTIME_IDENTIFIER = "RUNTIME_IDENTIFIER"; const string HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY = "APP_CONTEXT_BASE_DIRECTORY"; - sealed class DSOCacheEntryContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var dso_entry = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("hash", fieldName)) { - return $" from name: {dso_entry.HashedName}"; - } - - if (MonoAndroidHelper.StringEquals ("name_index", fieldName)) { - return $" name: {dso_entry.RealName}"; - } - - return String.Empty; - } - } - - // Disable "Field 'X' is never assigned to, and will always have its default value Y" - // Classes below are used in native code generation, thus all the fields must be present - // but they aren't always assigned values (which is fine). - #pragma warning disable CS0649 + // The structure declarations written by this class must be identical to the structures in + // src/native/clr/include/xamarin-app.hh. Data sizes are the sums of sizes of all the non-pointer + // members (see LlvmIrTarget.GetAggregateAlignment). All of the structures contain pointers, and + // their non-pointer members don't need alignment higher than NonPointerMemberAlignment + const ulong ApplicationConfigDataSize = 52; + const ulong AssemblyStoreRuntimeDataDataSize = 8; + const ulong AssemblyStoreSingleAssemblyRuntimeDataDataSize = 0; + const ulong DSOCacheEntryDataSize = 10; + const ulong XamarinAndroidBundledAssemblyDataSize = 16; + const ulong NonPointerMemberAlignment = 4; - // Order of fields and their type must correspond *exactly* (with exception of the - // ignored managed members) to that in - // src/native/clr/include/xamarin-app.hh DSOCacheEntry structure - [NativeAssemblerStructContextDataProvider (typeof (DSOCacheEntryContextDataProvider))] sealed class DSOCacheEntry { - [NativeAssembler (Ignore = true)] - public string? HashedName; + public string HashedName = ""; + public string RealName = ""; - [NativeAssembler (Ignore = true)] - public string? RealName; - - [NativeAssembler (UsesDataProvider = true, NumberFormat = LlvmIrVariableNumberFormat.Hexadecimal)] public uint hash; - public bool ignore; public bool is_jni_library; - - [NativeAssembler (UsesDataProvider = true)] public uint name_index; - public IntPtr handle = IntPtr.Zero; } - // Order of fields and their type must correspond *exactly* to that in - // src/monodroid/jni/xamarin-app.hh AssemblyStoreAssemblyDescriptor structure - sealed class AssemblyStoreAssemblyDescriptor + sealed class State { - public uint data_offset; - public uint data_size; - - public uint debug_data_offset; - public uint debug_data_size; - - public uint config_data_offset; - public uint config_data_size; - } - - // Order of fields and their type must correspond *exactly* to that in - // src/monodroid/jni/xamarin-app.hh AssemblyStoreSingleAssemblyRuntimeData structure - sealed class AssemblyStoreSingleAssemblyRuntimeData - { - [NativePointer] - public byte image_data; - - [NativePointer] - public byte debug_info_data; - - [NativePointer] - public byte config_data; - - [NativePointer] - public AssemblyStoreAssemblyDescriptor? descriptor; - } - - // Order of fields and their type must correspond *exactly* to that in - // src/native/clr/include/xamarin-app.hh AssemblyStoreRuntimeData structure - sealed class AssemblyStoreRuntimeData - { - [NativePointer (IsNull = true)] - public byte data_start; - public uint assembly_count; - public uint index_entry_count; - - [NativePointer (IsNull = true)] - public AssemblyStoreAssemblyDescriptor? assemblies; - } + public readonly AppEnvironmentVariableTable EnvironmentVariables; + public readonly AppEnvironmentVariableTable SystemProperties; + public ApplicationConfig ApplicationConfig = new (); + public readonly List DsoCache = []; + public readonly List JniPreloadDSOs = []; + public readonly LlvmIrStringBlob NamesBlob = new (); + public uint NameMutationsCount = 1; + public readonly List RuntimePropertyNames = []; + public readonly List RuntimePropertyValues = []; - sealed class XamarinAndroidBundledAssemblyContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override ulong GetBufferSize (object data, string fieldName) + public State (AppEnvironmentVariableTable environmentVariables, AppEnvironmentVariableTable systemProperties) { - var xaba = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("name", fieldName)) { - return xaba.name_length; - } - - if (MonoAndroidHelper.StringEquals ("file_name", fieldName)) { - return xaba.name_length + MonoAndroidHelper.GetMangledAssemblyNameSizeOverhead (); - } - - return 0; + EnvironmentVariables = environmentVariables; + SystemProperties = systemProperties; } } - // Order of fields and their type must correspond *exactly* to that in - // src/monodroid/jni/xamarin-app.hh XamarinAndroidBundledAssembly structure - [NativeAssemblerStructContextDataProvider (typeof (XamarinAndroidBundledAssemblyContextDataProvider))] - sealed class XamarinAndroidBundledAssembly - { - public int file_fd; - - [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToPreAllocatedBuffer = true)] - public string? file_name; - public uint data_offset; - public uint data_size; - - [NativePointer] - public byte data; - public uint name_length; - - [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToPreAllocatedBuffer = true)] - public string? name; - } -#pragma warning restore CS0649 - - sealed class DsoCacheState - { - public List> DsoCache = []; - public List JniPreloadDSOs = []; - public List JniPreloadNames = []; - public LlvmIrStringBlob NamesBlob = null!; - public uint NameMutationsCount = 1; - } - // Keep in sync with FORMAT_TAG in src/monodroid/jni/xamarin-app.hh const ulong FORMAT_TAG = 0x00025E6972616D58; // 'Xmari^XY' where XY is the format version @@ -165,21 +70,17 @@ sealed class DsoCacheState "libmonodroid.so", }; - SortedDictionary ? environmentVariables; - SortedDictionary ? systemProperties; - SortedDictionary ? runtimeProperties; - StructureInstance? application_config; - -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - assigned conditionally by build process - List>? xamarinAndroidBundledAssemblies; -#pragma warning restore CS0649 + readonly TaskLoggingHelper Log; + readonly SortedDictionary ? environmentVariables; + readonly SortedDictionary ? systemProperties; + readonly SortedDictionary runtimeProperties; + State? state; - StructureInfo? applicationConfigStructureInfo; - StructureInfo? dsoCacheEntryStructureInfo; - StructureInfo? xamarinAndroidBundledAssemblyStructureInfo; - StructureInfo? assemblyStoreSingleAssemblyRuntimeDataStructureinfo; - StructureInfo? assemblyStoreRuntimeDataStructureInfo; - StructureInfo? appEnvironmentVariableStructureInfo; + /// + /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. + /// + public bool EmitComments { get; set; } public bool UsesAssemblyPreload { get; set; } public string AndroidPackageName { get; set; } = ""; @@ -199,8 +100,9 @@ sealed class DsoCacheState public ApplicationConfigNativeAssemblyGenerator (IDictionary environmentVariables, IDictionary systemProperties, IDictionary? runtimeProperties, TaskLoggingHelper log) - : base (log) { + Log = log ?? throw new ArgumentNullException (nameof (log)); + if (environmentVariables != null) { this.environmentVariables = new SortedDictionary (environmentVariables, StringComparer.Ordinal); } @@ -225,56 +127,36 @@ public ApplicationConfigNativeAssemblyGenerator (IDictionary env this.runtimeProperties.Remove (HOST_PROPERTY_BUNDLE_PROBE); } - protected override void Construct (LlvmIrModule module) - { - MapStructures (module); + /// + /// Performs the architecture independent part of the work (e.g. scanning native libraries), which + /// may fail on invalid input. Called by if not called explicitly before. + /// + public void Initialize () => EnsureState (); - module.AddGlobalVariable ("format_tag", FORMAT_TAG, comment: $" 0x{FORMAT_TAG:x}"); + State EnsureState () => state ??= Init (); - var envVarsBlob = new LlvmIrStringBlob (); - List> appEnvVars = LlvmIrHelpers.MakeEnvironmentVariableList ( - Log, - environmentVariables, - envVarsBlob, - appEnvironmentVariableStructureInfo - ); - - var envVars = new LlvmIrGlobalVariable (appEnvVars, "app_environment_variables") { - Comment = " Application environment variables array, name:value", - Options = LlvmIrVariableOptions.GlobalConstant, - }; - module.Add (envVars); - module.AddGlobalVariable ("app_environment_variable_contents", envVarsBlob, LlvmIrVariableOptions.GlobalConstant); + State Init () + { + var envVars = new AppEnvironmentVariableTable (Log, environmentVariables); // We reuse the same structure as for environment variables, there's no point in adding a new, identical, one - var sysPropsBlob = new LlvmIrStringBlob (); - List> appSysProps = LlvmIrHelpers.MakeEnvironmentVariableList ( - Log, - systemProperties, - sysPropsBlob, - appEnvironmentVariableStructureInfo - ); + var sysProps = new AppEnvironmentVariableTable (Log, systemProperties); - var sysProps = new LlvmIrGlobalVariable (appSysProps, "app_system_properties") { - Comment = " System properties defined by the application", - Options = LlvmIrVariableOptions.GlobalConstant, - }; - module.Add (sysProps); - module.AddGlobalVariable ("app_system_property_contents", sysPropsBlob, LlvmIrVariableOptions.GlobalConstant); + var ret = new State (envVars, sysProps); + InitDSOCache (ret); - DsoCacheState dsoState = InitDSOCache (); - var app_cfg = new ApplicationConfig { + ret.ApplicationConfig = new ApplicationConfig { uses_assembly_preload = UsesAssemblyPreload, marshal_methods_enabled = MarshalMethodsEnabled, ignore_split_configs = IgnoreSplitConfigs, - number_of_runtime_properties = (uint)(runtimeProperties == null ? 0 : runtimeProperties.Count), + number_of_runtime_properties = (uint)runtimeProperties.Count, package_naming_policy = (uint)PackageNamingPolicy, environment_variable_count = (uint)(environmentVariables == null ? 0 : environmentVariables.Count), - system_property_count = (uint)(appSysProps.Count), + system_property_count = (uint)sysProps.Count, number_of_assemblies_in_apk = (uint)NumberOfAssembliesInApk, number_of_shared_libraries = (uint)NativeLibraries.Count, bundled_assembly_name_width = (uint)BundledAssemblyNameWidth, - number_of_dso_cache_entries = (uint)dsoState.DsoCache.Count, + number_of_dso_cache_entries = (uint)ret.DsoCache.Count, android_runtime_jnienv_class_token = (uint)AndroidRuntimeJNIEnvToken, jnienv_initialize_method_token = (uint)JNIEnvInitializeToken, jni_remapping_replacement_type_count = (uint)JniRemappingReplacementTypeCount, @@ -282,168 +164,238 @@ protected override void Construct (LlvmIrModule module) android_package_name = AndroidPackageName, have_assembly_store = HaveAssemblyStore, }; - application_config = new StructureInstance (applicationConfigStructureInfo, app_cfg); - module.AddGlobalVariable ("application_config", application_config); - - var dso_cache = new LlvmIrGlobalVariable (dsoState.DsoCache, "dso_cache", LlvmIrVariableOptions.GlobalWritable) { - Comment = " DSO cache entries", - BeforeWriteCallback = HashAndSortDSOCache, - }; - module.Add (dso_cache); - - module.AddGlobalVariable ("dso_jni_preloads_idx_stride", dsoState.NameMutationsCount); - - // This variable MUST be written after `dso_cache` since it relies on sorting performed by HashAndSortDSOCache - var dso_jni_preloads_idx = new LlvmIrGlobalVariable (typeof (List), "dso_jni_preloads_idx", LlvmIrVariableOptions.GlobalConstant) { - Comment = " Indices into dso_cache[] of DSO libraries to preload because of JNI use", - ArrayItemCount = (uint)dsoState.JniPreloadDSOs.Count, - GetArrayItemCommentCallback = GetPreloadIndicesLibraryName, - GetArrayItemCommentCallbackCallerState = dsoState, - BeforeWriteCallback = PopulatePreloadIndices, - BeforeWriteCallbackCallerState = dsoState, - }; - module.AddGlobalVariable ("dso_jni_preloads_idx_count", dso_jni_preloads_idx.ArrayItemCount); - module.Add (dso_jni_preloads_idx); - - module.AddGlobalVariable ("dso_names_data", dsoState.NamesBlob, LlvmIrVariableOptions.GlobalConstant); - - string bundledBuffersSize = xamarinAndroidBundledAssemblies == null ? "empty (unused when assembly stores are enabled)" : $"{BundledAssemblyNameWidth} bytes long"; - var bundled_assemblies = new LlvmIrGlobalVariable (typeof(List>), "bundled_assemblies", LlvmIrVariableOptions.GlobalWritable) { - Value = xamarinAndroidBundledAssemblies, - Comment = $" Bundled assembly name buffers, all {bundledBuffersSize}", - }; - module.Add (bundled_assemblies); // HOST_PROPERTY_RUNTIME_CONTRACT, HOST_PROPERTY_RUNTIME_IDENTIFIER and // HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY will come first, in that order, our native runtime // requires that since it needs to set their values in the values array and we don't want to // spend time searching for the indices, nor we want to add yet another variable storing the // index to the entry. KISS. - var runtime_property_names = new List { - HOST_PROPERTY_RUNTIME_CONTRACT, - HOST_PROPERTY_RUNTIME_IDENTIFIER, - HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY, - }; - var runtime_property_values = new List { - null, - null, - null, - }; - - if (runtimeProperties != null) { - foreach (var kvp in runtimeProperties) { - if (MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_RUNTIME_CONTRACT) || - MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_RUNTIME_IDENTIFIER) || - MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY)) { - continue; - } - runtime_property_names.Add (kvp.Key); - runtime_property_values.Add (kvp.Value); + ret.RuntimePropertyNames.Add (HOST_PROPERTY_RUNTIME_CONTRACT); + ret.RuntimePropertyNames.Add (HOST_PROPERTY_RUNTIME_IDENTIFIER); + ret.RuntimePropertyNames.Add (HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY); + ret.RuntimePropertyValues.Add (null); + ret.RuntimePropertyValues.Add (null); + ret.RuntimePropertyValues.Add (null); + + foreach (var kvp in runtimeProperties) { + if (MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_RUNTIME_CONTRACT) || + MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_RUNTIME_IDENTIFIER) || + MonoAndroidHelper.StringEquals (kvp.Key, HOST_PROPERTY_APP_CONTEXT_BASE_DIRECTORY)) { + continue; } + ret.RuntimePropertyNames.Add (kvp.Key); + ret.RuntimePropertyValues.Add (kvp.Value); } - var init_runtime_property_names = new LlvmIrGlobalVariable (runtime_property_names, "init_runtime_property_names", LlvmIrVariableOptions.GlobalConstant) { - Comment = "Names of properties passed to coreclr_initialize", - }; - module.Add (init_runtime_property_names); - - var init_runtime_property_values = new LlvmIrGlobalVariable (runtime_property_values, "init_runtime_property_values", LlvmIrVariableOptions.GlobalWritable) { - Comment = "Values of properties passed to coreclr_initialize", - }; - module.Add (init_runtime_property_values); - - AddAssemblyStores (module); + return ret; } - void AddAssemblyStores (LlvmIrModule module) + public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - ulong itemCount = (ulong)(NumberOfAssembliesInApk); - var assembly_store_bundled_assemblies = new LlvmIrGlobalVariable (typeof(List>), "assembly_store_bundled_assemblies", LlvmIrVariableOptions.GlobalWritable) { - ZeroInitializeArray = true, - ArrayItemCount = itemCount, - }; - module.Add (assembly_store_bundled_assemblies); + State data = EnsureState (); + + var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + var strings = new LlvmIrStringPool (); + ulong structAlignment = Math.Max (w.Target.PointerSize, NonPointerMemberAlignment); + + w.WriteHeader (fileName); + AppEnvironmentVariableTable.WriteDeclaration (w); + w.Write ($$""" + + %struct.ApplicationConfig = type { + i1, {{w.Comment (" bool uses_assembly_preload")}} + i1, {{w.Comment (" bool marshal_methods_enabled")}} + i1, {{w.Comment (" bool ignore_split_configs")}} + i32, {{w.Comment (" uint32_t number_of_runtime_properties")}} + i32, {{w.Comment (" uint32_t package_naming_policy")}} + i32, {{w.Comment (" uint32_t environment_variable_count")}} + i32, {{w.Comment (" uint32_t system_property_count")}} + i32, {{w.Comment (" uint32_t number_of_assemblies_in_apk")}} + i32, {{w.Comment (" uint32_t bundled_assembly_name_width")}} + i32, {{w.Comment (" uint32_t number_of_dso_cache_entries")}} + i32, {{w.Comment (" uint32_t number_of_shared_libraries")}} + i32, {{w.Comment (" uint32_t android_runtime_jnienv_class_token")}} + i32, {{w.Comment (" uint32_t jnienv_initialize_method_token")}} + i32, {{w.Comment (" uint32_t jni_remapping_replacement_type_count")}} + i32, {{w.Comment (" uint32_t jni_remapping_replacement_method_index_entry_count")}} + ptr, {{w.Comment (" char* android_package_name")}} + i1 {{w.Comment (" bool have_assembly_store")}} + } - var storeRuntimeData = new AssemblyStoreRuntimeData { - data_start = 0, - assembly_count = 0, - }; + %struct.AssemblyStoreAssemblyDescriptor = type { + i32, {{w.Comment (" uint32_t data_offset")}} + i32, {{w.Comment (" uint32_t data_size")}} + i32, {{w.Comment (" uint32_t debug_data_offset")}} + i32, {{w.Comment (" uint32_t debug_data_size")}} + i32, {{w.Comment (" uint32_t config_data_offset")}} + i32 {{w.Comment (" uint32_t config_data_size")}} + } - var assembly_store = new LlvmIrGlobalVariable ( - new StructureInstance(assemblyStoreRuntimeDataStructureInfo, storeRuntimeData), - "assembly_store", - LlvmIrVariableOptions.GlobalWritable - ); - module.Add (assembly_store); - } + %struct.AssemblyStoreRuntimeData = type { + ptr, {{w.Comment (" uint8_t data_start")}} + i32, {{w.Comment (" uint32_t assembly_count")}} + i32, {{w.Comment (" uint32_t index_entry_count")}} + ptr {{w.Comment (" AssemblyStoreAssemblyDescriptor assemblies")}} + } - string? GetPreloadIndicesLibraryName (LlvmIrVariable v, LlvmIrModuleTarget target, ulong index, object? value, object? callerState) - { - // Instead of throwing for such a triviality like a comment, we will return error messages as comments instead - var dsoState = callerState as DsoCacheState; - if (dsoState == null) { - return " Internal error: DSO state not present."; - } + %struct.AssemblyStoreSingleAssemblyRuntimeData = type { + ptr, {{w.Comment (" uint8_t image_data")}} + ptr, {{w.Comment (" uint8_t debug_info_data")}} + ptr, {{w.Comment (" uint8_t config_data")}} + ptr {{w.Comment (" AssemblyStoreAssemblyDescriptor descriptor")}} + } - if (index >= (ulong)dsoState.JniPreloadNames.Count) { - return $" Invalid index {index}"; - } + %struct.DSOCacheEntry = type { + i32, {{w.Comment (" uint32_t hash")}} + i1, {{w.Comment (" bool ignore")}} + i1, {{w.Comment (" bool is_jni_library")}} + i32, {{w.Comment (" uint32_t name_index")}} + ptr {{w.Comment (" void* handle")}} + } - return $" {dsoState.JniPreloadNames[(int)index]}"; - } + %struct.XamarinAndroidBundledAssembly = type { + i32, {{w.Comment (" int32_t file_fd")}} + ptr, {{w.Comment (" char* file_name")}} + i32, {{w.Comment (" uint32_t data_offset")}} + i32, {{w.Comment (" uint32_t data_size")}} + ptr, {{w.Comment (" uint8_t data")}} + i32, {{w.Comment (" uint32_t name_length")}} + ptr {{w.Comment (" char* name")}} + } - void PopulatePreloadIndices (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state) - { - var dsoState = state as DsoCacheState; - if (dsoState == null) { - throw new InvalidOperationException ("Internal error: DSO state not present."); - } + """); + + w.WriteGlobal ("format_tag", LlvmIrWriter.GlobalConstant, "i64", LlvmIrWriter.Number (FORMAT_TAG), 8, $" 0x{FORMAT_TAG:x}"); + + data.EnvironmentVariables.Write (w, "app_environment_variables", "app_environment_variable_contents", " Application environment variables array, name:value"); + data.SystemProperties.Write (w, "app_system_properties", "app_system_property_contents", " System properties defined by the application"); + + ApplicationConfig cfg = data.ApplicationConfig; + w.WriteGlobal ("application_config", LlvmIrWriter.GlobalConstant, "%struct.ApplicationConfig", $$""" + { + i1 {{LlvmIrWriter.Bool (cfg.uses_assembly_preload)}}, {{w.Comment (" bool uses_assembly_preload")}} + i1 {{LlvmIrWriter.Bool (cfg.marshal_methods_enabled)}}, {{w.Comment (" bool marshal_methods_enabled")}} + i1 {{LlvmIrWriter.Bool (cfg.ignore_split_configs)}}, {{w.Comment (" bool ignore_split_configs")}} + i32 {{LlvmIrWriter.Number (cfg.number_of_runtime_properties)}}, {{w.Comment (" uint32_t number_of_runtime_properties")}} + i32 {{LlvmIrWriter.Number (cfg.package_naming_policy)}}, {{w.Comment (" uint32_t package_naming_policy")}} + i32 {{LlvmIrWriter.Number (cfg.environment_variable_count)}}, {{w.Comment (" uint32_t environment_variable_count")}} + i32 {{LlvmIrWriter.Number (cfg.system_property_count)}}, {{w.Comment (" uint32_t system_property_count")}} + i32 {{LlvmIrWriter.Number (cfg.number_of_assemblies_in_apk)}}, {{w.Comment (" uint32_t number_of_assemblies_in_apk")}} + i32 {{LlvmIrWriter.Number (cfg.bundled_assembly_name_width)}}, {{w.Comment (" uint32_t bundled_assembly_name_width")}} + i32 {{LlvmIrWriter.Number (cfg.number_of_dso_cache_entries)}}, {{w.Comment (" uint32_t number_of_dso_cache_entries")}} + i32 {{LlvmIrWriter.Number (cfg.number_of_shared_libraries)}}, {{w.Comment (" uint32_t number_of_shared_libraries")}} + i32 {{LlvmIrWriter.Hex (cfg.android_runtime_jnienv_class_token)}}, {{w.Comment (" uint32_t android_runtime_jnienv_class_token")}} + i32 {{LlvmIrWriter.Hex (cfg.jnienv_initialize_method_token)}}, {{w.Comment (" uint32_t jnienv_initialize_method_token")}} + i32 {{LlvmIrWriter.Hex (cfg.jni_remapping_replacement_type_count)}}, {{w.Comment (" uint32_t jni_remapping_replacement_type_count")}} + i32 {{LlvmIrWriter.Number (cfg.jni_remapping_replacement_method_index_entry_count)}}, {{w.Comment (" uint32_t jni_remapping_replacement_method_index_entry_count")}} + ptr {{strings.GetPointer (cfg.android_package_name, "ApplicationConfig", "android_package_name")}}, {{w.Comment (" char* android_package_name")}} + i1 {{LlvmIrWriter.Bool (cfg.have_assembly_store)}}{{w.Comment (" bool have_assembly_store")}} + } + """, w.GetAggregateAlignment (structAlignment, ApplicationConfigDataSize)); - var dsoNames = new List (); + WriteDsoCache (w, data, structAlignment); - // Indices array MUST NOT be sorted, since it groups alias entries together with the main entry - var indices = new List (); - variable.Value = indices; - foreach (DSOCacheEntry preload in dsoState.JniPreloadDSOs) { - int dsoIdx = dsoState.DsoCache.FindIndex (entry => ReferenceEquals (entry.Instance, preload)); + w.WriteGlobal ("bundled_assemblies", LlvmIrWriter.GlobalWritable, "[0 x %struct.XamarinAndroidBundledAssembly]", "zeroinitializer", w.GetAggregateAlignment (structAlignment, 0 * XamarinAndroidBundledAssemblyDataSize), " Bundled assembly name buffers, all empty (unused when assembly stores are enabled)"); - if (dsoIdx == -1) { - throw new InvalidOperationException ($"Internal error: DSO entry in JNI preload list not found in the DSO cache list."); - } + var names = new List (data.RuntimePropertyNames.Count); + foreach (string name in data.RuntimePropertyNames) { + names.Add ($"\tptr {strings.GetPointer (name)}"); + } + w.WriteGlobal ( + "init_runtime_property_names", + LlvmIrWriter.GlobalConstant, + $"[{LlvmIrWriter.Number (names.Count)} x ptr]", + w.ArrayValue (names, i => $" {LlvmIrWriter.Number (i)} ('{data.RuntimePropertyNames [i]}')"), + w.GetPointerArrayAlignment (names.Count), + "Names of properties passed to coreclr_initialize" + ); - indices.Add ((uint)dsoIdx); - dsoNames.Add (preload.HashedName ?? String.Empty); + var values = new List (data.RuntimePropertyValues.Count); + foreach (string? value in data.RuntimePropertyValues) { + values.Add ($"\tptr {strings.GetPointer (value)}"); } - dsoState.JniPreloadNames = dsoNames; + w.WriteGlobal ( + "init_runtime_property_values", + LlvmIrWriter.GlobalWritable, + $"[{LlvmIrWriter.Number (values.Count)} x ptr]", + w.ArrayValue (values, i => $" {LlvmIrWriter.Number (i)} ('{data.RuntimePropertyValues [i]}')"), + w.GetPointerArrayAlignment (values.Count), + "Values of properties passed to coreclr_initialize" + ); + + ulong assemblyCount = (ulong)NumberOfAssembliesInApk; + w.WriteGlobal ("assembly_store_bundled_assemblies", LlvmIrWriter.GlobalWritable, $"[{LlvmIrWriter.Number (assemblyCount)} x %struct.AssemblyStoreSingleAssemblyRuntimeData]", "zeroinitializer", w.GetAggregateAlignment (w.Target.PointerSize, assemblyCount * AssemblyStoreSingleAssemblyRuntimeDataDataSize)); + w.WriteGlobal ("assembly_store", LlvmIrWriter.GlobalWritable, "%struct.AssemblyStoreRuntimeData", $$""" + { + ptr null, {{w.Comment (" uint8_t* data_start")}} + i32 0, {{w.Comment (" uint32_t assembly_count")}} + i32 0, {{w.Comment (" uint32_t index_entry_count")}} + ptr null{{w.Comment (" AssemblyStoreAssemblyDescriptor* assemblies")}} + } + """, w.GetAggregateAlignment (structAlignment, AssemblyStoreRuntimeDataDataSize)); + + strings.Write (w); + w.WriteMetadata (); + output.Flush (); } - void HashAndSortDSOCache (LlvmIrVariable variable, LlvmIrModuleTarget target, object? state) + void WriteDsoCache (LlvmIrWriter w, State state, ulong structAlignment) { - var cache = variable.Value as List>; - if (cache == null) { - throw new InvalidOperationException ($"Internal error: DSO cache must not be empty"); + // Hashes are architecture independent, but the sort is repeated for every architecture as it + // determines the order of entries with identical hashes + state.DsoCache.Sort ((DSOCacheEntry a, DSOCacheEntry b) => a.hash.CompareTo (b.hash)); + + var entries = new List (state.DsoCache.Count); + foreach (DSOCacheEntry entry in state.DsoCache) { + entries.Add ($$""" + %struct.DSOCacheEntry { + i32 {{LlvmIrWriter.Hex (entry.hash)}}, {{w.Comment ($" from name: {entry.HashedName}")}} + i1 {{LlvmIrWriter.Bool (entry.ignore)}}, {{w.Comment (" bool ignore")}} + i1 {{LlvmIrWriter.Bool (entry.is_jni_library)}}, {{w.Comment (" bool is_jni_library")}} + i32 {{LlvmIrWriter.Number (entry.name_index)}}, {{w.Comment ($" name: {entry.RealName}")}} + ptr null{{w.Comment (" void* handle")}} + } + """); } + w.WriteGlobal ( + "dso_cache", + LlvmIrWriter.GlobalWritable, + $"[{LlvmIrWriter.Number (entries.Count)} x %struct.DSOCacheEntry]", + w.ArrayValue (entries, LlvmIrWriter.IndexComment), + w.GetAggregateAlignment (structAlignment, (ulong)entries.Count * DSOCacheEntryDataSize), + " DSO cache entries" + ); - foreach (StructureInstance instance in cache) { - if (instance.Obj == null) { - throw new InvalidOperationException ("Internal error: DSO cache must not contain null entries"); - } + w.WriteGlobal ("dso_jni_preloads_idx_stride", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (state.NameMutationsCount), 4); - var entry = instance.Obj as DSOCacheEntry; - if (entry == null) { - throw new InvalidOperationException ($"Internal error: DSO cache entry has unexpected type {instance.Obj.GetType ()}"); + // Indices array MUST NOT be sorted, since it groups alias entries together with the main entry + var indices = new List (state.JniPreloadDSOs.Count); + var indexNames = new List (state.JniPreloadDSOs.Count); + foreach (DSOCacheEntry preload in state.JniPreloadDSOs) { + int dsoIdx = state.DsoCache.FindIndex (entry => ReferenceEquals (entry, preload)); + if (dsoIdx == -1) { + throw new InvalidOperationException ($"Internal error: DSO entry in JNI preload list not found in the DSO cache list."); } - entry.hash = TypeMapHelper.HashNameForCLR (entry.HashedName ?? ""); + indices.Add ($"\ti32 {LlvmIrWriter.Number (dsoIdx)}"); + indexNames.Add (preload.HashedName); } - cache.Sort ((StructureInstance a, StructureInstance b) => { - if (a.Instance == null || b.Instance == null) return 0; - return a.Instance.hash.CompareTo (b.Instance.hash); - }); + // Historically, the count has always been a 64-bit integer + w.WriteGlobal ("dso_jni_preloads_idx_count", LlvmIrWriter.GlobalConstant, "i64", LlvmIrWriter.Number (indices.Count), 8); + w.WriteGlobal ( + "dso_jni_preloads_idx", + LlvmIrWriter.GlobalConstant, + $"[{LlvmIrWriter.Number (indices.Count)} x i32]", + w.ArrayValue (indices, i => $" {indexNames [i]}"), + w.GetAggregateAlignment (4, (ulong)indices.Count * 4), + " Indices into dso_cache[] of DSO libraries to preload because of JNI use" + ); + + state.NamesBlob.Write (w, "dso_names_data"); } - DsoCacheState InitDSOCache () + void InitDSOCache (State state) { var dsos = new List<(string name, ITaskItem item)> (); var nameCache = new HashSet (StringComparer.OrdinalIgnoreCase); @@ -462,17 +414,14 @@ DsoCacheState InitDSOCache () dsos.Add ((name, item)); } - var dsoCache = new List> (); - var jniPreloads = new List (); var nameMutations = new List (); - var dsoNamesBlob = new LlvmIrStringBlob (); int nameMutationsCount = -1; ICollection ignorePreload = MakeJniPreloadIgnoreCollection (Log, NativeLibrariesAlwaysJniPreload, NativeLibrariesNoJniPreload); for (int i = 0; i < dsos.Count; i++) { string name = dsos[i].name; - (int nameOffset, _) = dsoNamesBlob.Add (name); + int nameOffset = state.NamesBlob.Add (name); bool isJniLibrary = ELFHelper.IsJniLibrary (Log, dsos[i].item.ItemSpec); bool ignore_for_preload = ShouldIgnoreForJniPreload (Log, ignorePreload, dsos[i].item); @@ -489,30 +438,23 @@ DsoCacheState InitDSOCache () HashedName = entryName, RealName = name, - hash = 0, // Hash is arch-specific, we compute it before writing + hash = TypeMapHelper.HashNameForCLR (entryName), ignore = false, is_jni_library = isJniLibrary, name_index = (uint)nameOffset, }; - var item = new StructureInstance (dsoCacheEntryStructureInfo, entry); - // We must add all aliases to the preloads indices array so that all of them have their handle // set when the library is preloaded. if (entry.is_jni_library && !ignore_for_preload) { - jniPreloads.Add (entry); + state.JniPreloadDSOs.Add (entry); } - dsoCache.Add (item); + state.DsoCache.Add (entry); } } - return new DsoCacheState { - DsoCache = dsoCache, - JniPreloadDSOs = jniPreloads, - NamesBlob = dsoNamesBlob, - NameMutationsCount = (uint)(nameMutationsCount <= 0 ? 1 : nameMutationsCount), - }; + state.NameMutationsCount = (uint)(nameMutationsCount <= 0 ? 1 : nameMutationsCount); void AddNameMutations (string name) { @@ -521,11 +463,11 @@ void AddNameMutations (string name) // src/native/clr/include/runtime-base/monodroid-dl.hh). Keep the two in sync. nameMutations.Add (name); if (name.EndsWith (".dll.so", StringComparison.OrdinalIgnoreCase)) { - string nameNoExt = Path.GetFileNameWithoutExtension (Path.GetFileNameWithoutExtension (name))!; + string nameNoExt = Path.GetFileNameWithoutExtension (Path.GetFileNameWithoutExtension (name)); nameMutations.Add (nameNoExt); nameMutations.Add ($"{nameNoExt}.so"); } else { - nameMutations.Add (Path.GetFileNameWithoutExtension (name)!); + nameMutations.Add (Path.GetFileNameWithoutExtension (name)); } const string libPrefix = "lib"; @@ -535,17 +477,6 @@ void AddNameMutations (string name) } } - void MapStructures (LlvmIrModule module) - { - applicationConfigStructureInfo = module.MapStructure (); - module.MapStructure (); - assemblyStoreSingleAssemblyRuntimeDataStructureinfo = module.MapStructure (); - assemblyStoreRuntimeDataStructureInfo = module.MapStructure (); - xamarinAndroidBundledAssemblyStructureInfo = module.MapStructure (); - dsoCacheEntryStructureInfo = module.MapStructure (); - appEnvironmentVariableStructureInfo = module.MapStructure (); - } - internal static bool ShouldIgnoreForJniPreload (TaskLoggingHelper log, ICollection libsToIgnore, ITaskItem libItem) { if (libsToIgnore.Count == 0) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs index 93b695662b6..d209f3d1245 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs @@ -2,197 +2,146 @@ using System; using System.Collections.Generic; +using System.IO; using Microsoft.Build.Utilities; -using Xamarin.Android.Tasks.LLVMIR; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks { - partial class CompressedAssembliesNativeAssemblyGenerator : LlvmIrComposer + class CompressedAssembliesNativeAssemblyGenerator { const string DescriptorsArraySymbolName = "compressed_assembly_descriptors"; const string CompressedAssemblyCountSymbolName = "compressed_assembly_count"; const string UncompressedAssembliesBufferSymbolName = "uncompressed_assemblies_data_buffer"; const string UncompressedAssembliesBufferSizeSymbolName = "uncompressed_assemblies_data_size"; - // Order of fields and their type must correspond *exactly* to that in: - // - // src/native/clr/include/xamarin-app.hh CompressedAssemblyDescriptor structure - // - //[NativeAssemblerStructContextDataProvider (typeof (CompressedAssemblyDescriptorContextDataProvider))] + // The %struct.CompressedAssemblyDescriptor declaration below must be identical to the + // CompressedAssemblyDescriptor structure in src/native/clr/include/xamarin-app.hh + // See LlvmIrTarget.GetAggregateAlignment for the meaning of the "data size" + const ulong DescriptorDataSize = 9; + const ulong DescriptorAlignment = 4; + sealed class CompressedAssemblyDescriptor { - [NativeAssembler (Ignore = true)] public uint Index; + public string AssemblyName = ""; - [NativeAssembler (Ignore = true)] - public string? AssemblyName; + public uint uncompressed_file_size; + public uint buffer_offset; + } - public uint uncompressed_file_size; - public bool loaded; - public uint buffer_offset; - }; + readonly Dictionary> archDescriptors = new (); + readonly Dictionary archBufferSizes = new (); - IDictionary>? archAssemblies; - StructureInfo? compressedAssemblyDescriptorStructureInfo; - Dictionary>> archData = new Dictionary>> (); + /// + /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// + public bool EmitComments { get; set; } public CompressedAssembliesNativeAssemblyGenerator (TaskLoggingHelper log, IDictionary>? archAssemblies) - : base (log) { - this.archAssemblies = archAssemblies; - } + if (log == null) { + throw new ArgumentNullException (nameof (log)); + } - void InitCompressedAssemblies (out List? compressedAssemblies, - out List? compressedAssemblyDescriptors, - out List? buffers) - { if (archAssemblies == null || archAssemblies.Count == 0) { - compressedAssemblies = null; - compressedAssemblyDescriptors = null; - buffers = null; return; } - buffers = new (); foreach (var kvpArch in archAssemblies) { uint bufferSize = 0; foreach (var kvp in kvpArch.Value) { CompressedAssemblyInfo info = kvp.Value; - if (!archData.TryGetValue (info.TargetArch, out List> descriptors)) { - descriptors = new List> (); - archData.Add (info.TargetArch, descriptors); + if (!archDescriptors.TryGetValue (info.TargetArch, out List? descriptors)) { + descriptors = new List (); + archDescriptors.Add (info.TargetArch, descriptors); } - var descriptor = new CompressedAssemblyDescriptor { + descriptors.Add (new CompressedAssemblyDescriptor { Index = info.DescriptorIndex, AssemblyName = info.AssemblyName, uncompressed_file_size = info.FileSize, - loaded = false, buffer_offset = bufferSize, - }; + }); bufferSize += info.FileSize; - descriptors.Add (new StructureInstance (compressedAssemblyDescriptorStructureInfo, descriptor)); } - var variable = new LlvmIrGlobalVariable (typeof(uint), UncompressedAssembliesBufferSizeSymbolName) { - Options = LlvmIrVariableOptions.GlobalConstant, - TargetArch = kvpArch.Key, - Value = bufferSize, - }; - buffers.Add (variable); - - variable = new LlvmIrGlobalVariable (typeof(List), UncompressedAssembliesBufferSymbolName, LlvmIrVariableOptions.GlobalWritable) { - ArrayItemCount = bufferSize, - TargetArch = kvpArch.Key, - ZeroInitializeArray = true, - }; - buffers.Add (variable); + archBufferSizes [kvpArch.Key] = bufferSize; } - compressedAssemblies = new List (); - compressedAssemblyDescriptors = new List (); - foreach (var kvp in archData) { - List> descriptors = kvp.Value; - descriptors.Sort ((StructureInstance a, StructureInstance b) => { - if (a.Instance == null || b.Instance == null) return 0; - return a.Instance.Index.CompareTo (b.Instance.Index); - }); - - var variable = new LlvmIrGlobalVariable (typeof(uint), CompressedAssemblyCountSymbolName) { - Options = LlvmIrVariableOptions.GlobalConstant, - TargetArch = kvp.Key, - Value = (uint)descriptors.Count, - }; - compressedAssemblies.Add (variable); - - variable = new LlvmIrGlobalVariable (typeof(List>), DescriptorsArraySymbolName) { - GetArrayItemCommentCallback = GetCompressedAssemblyDescriptorsItemComment, - Options = LlvmIrVariableOptions.GlobalWritable, - TargetArch = kvp.Key, - Value = descriptors, - }; - compressedAssemblyDescriptors.Add (variable); + foreach (var kvp in archDescriptors) { + kvp.Value.Sort ((CompressedAssemblyDescriptor a, CompressedAssemblyDescriptor b) => a.Index.CompareTo (b.Index)); } } - protected override void Construct (LlvmIrModule module) + public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - module.DefaultStringGroup = "cas"; + var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + w.WriteHeader (fileName); + w.Write ($$""" + + %struct.CompressedAssemblyDescriptor = type { + i32, {{w.Comment (" uint32_t uncompressed_file_size")}} + i1, {{w.Comment (" bool loaded")}} + i32 {{w.Comment (" uint32_t buffer_offset")}} + } - MapStructures (module); + """); - InitCompressedAssemblies ( - out List? compressedAssemblies, - out List? compressedAssemblyDescriptors, - out List? buffers - ); + if (archDescriptors.Count == 0) { + WriteCount (w, 0); + WriteDescriptors (w, []); + WriteBuffer (w, 0); + } else { + if (archDescriptors.TryGetValue (arch, out List? descriptors)) { + WriteCount (w, (uint)descriptors.Count); + WriteDescriptors (w, descriptors); + } - if (archData.Count == 0) { - var emptyCountVar = new LlvmIrGlobalVariable (typeof(uint), CompressedAssemblyCountSymbolName) { - Options = LlvmIrVariableOptions.GlobalConstant, - Value = 0u, - }; - module.Add (emptyCountVar); - - var emptyDescriptorsVar = new LlvmIrGlobalVariable (typeof(List>), DescriptorsArraySymbolName) { - Options = LlvmIrVariableOptions.GlobalWritable, - Value = new List> (), - }; - module.Add (emptyDescriptorsVar); - - var emptyBufferSizeVar = new LlvmIrGlobalVariable (typeof(uint), UncompressedAssembliesBufferSizeSymbolName) { - Options = LlvmIrVariableOptions.GlobalConstant, - Value = 0u, - }; - module.Add (emptyBufferSizeVar); - - var emptyBufferVar = new LlvmIrGlobalVariable (typeof(List), UncompressedAssembliesBufferSymbolName, LlvmIrVariableOptions.GlobalWritable) { - ArrayItemCount = 0, - ZeroInitializeArray = true, - }; - module.Add (emptyBufferVar); - return; + if (archBufferSizes.TryGetValue (arch, out uint bufferSize)) { + WriteBuffer (w, bufferSize); + } } - if (compressedAssemblies != null) { - module.Add (compressedAssemblies); - } - if (compressedAssemblyDescriptors != null) { - module.Add (compressedAssemblyDescriptors); - } - if (buffers != null) { - module.Add (buffers); - } + w.WriteMetadata (); + output.Flush (); } - string? GetCompressedAssemblyDescriptorsItemComment (LlvmIrVariable v, LlvmIrModuleTarget target, ulong index, object? value, object? callerState) + static void WriteCount (LlvmIrWriter w, uint count) { - List> descriptors = GetArchDescriptors (target); - if ((int)index >= descriptors.Count) { - throw new InvalidOperationException ($"Internal error: index {index} is too big for variable '{v.Name}'"); - } - StructureInstance desc = descriptors[(int)index]; - - return $" {index}: {desc.Instance?.AssemblyName ?? ""}"; + w.WriteGlobal (CompressedAssemblyCountSymbolName, LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (count), 4); } - List> GetArchDescriptors (LlvmIrModuleTarget target) + static void WriteDescriptors (LlvmIrWriter w, List descriptors) { - if (!archData.TryGetValue (target.TargetArch, out List> descriptors)) { - throw new InvalidOperationException ($"Internal error: missing compressed descriptors data for architecture '{target.TargetArch}'"); + var elements = new List (descriptors.Count); + foreach (CompressedAssemblyDescriptor d in descriptors) { + elements.Add ($$""" + %struct.CompressedAssemblyDescriptor { + i32 {{LlvmIrWriter.Number (d.uncompressed_file_size)}}, {{w.Comment (" uint32_t uncompressed_file_size")}} + i1 false, {{w.Comment (" bool loaded")}} + i32 {{LlvmIrWriter.Number (d.buffer_offset)}}{{w.Comment (" uint32_t buffer_offset")}} + } + """); } - return descriptors; + w.WriteGlobal ( + DescriptorsArraySymbolName, + LlvmIrWriter.GlobalWritable, + $"[{LlvmIrWriter.Number (descriptors.Count)} x %struct.CompressedAssemblyDescriptor]", + w.ArrayValue (elements, i => $" {LlvmIrWriter.Number (i)}: {descriptors [i].AssemblyName}"), + w.GetAggregateAlignment (DescriptorAlignment, (ulong)descriptors.Count * DescriptorDataSize) + ); } - void MapStructures (LlvmIrModule module) + static void WriteBuffer (LlvmIrWriter w, uint bufferSize) { - compressedAssemblyDescriptorStructureInfo = module.MapStructure (); + w.WriteGlobal (UncompressedAssembliesBufferSizeSymbolName, LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (bufferSize), 4); + w.WriteGlobal (UncompressedAssembliesBufferSymbolName, LlvmIrWriter.GlobalWritable, $"[{LlvmIrWriter.Number (bufferSize)} x i8]", "zeroinitializer", w.GetAggregateAlignment (1, bufferSize)); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniOnLoadNativeAssemblerHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniOnLoadNativeAssemblerHelper.cs deleted file mode 100644 index fda10f12f2a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniOnLoadNativeAssemblerHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -using Xamarin.Android.Tasks.LLVMIR; - -namespace Xamarin.Android.Tasks; - -class JniOnLoadNativeAssemblerHelper -{ - public static void GenerateJniOnLoadHandlerCode (ICollection jniOnLoadNames, LlvmIrModule module) - { - module.AddGlobalVariable ("__jni_on_load_handler_count", (uint)jniOnLoadNames.Count, LlvmIrVariableOptions.GlobalConstant); - var jniOnLoadPointers = new List (); - foreach (string name in jniOnLoadNames) { - var symref = new LlvmIrGlobalVariableReference (name); - jniOnLoadPointers.Add (symref); - LlvmIrHelpers.DeclareDummyFunction (module, symref); - } - module.AddGlobalVariable ("__jni_on_load_handlers", jniOnLoadPointers, LlvmIrVariableOptions.GlobalConstant); - module.AddGlobalVariable ("__jni_on_load_handler_names", jniOnLoadNames, LlvmIrVariableOptions.GlobalConstant); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index c79f4855a58..90817da13a1 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -1,12 +1,13 @@ -#nullable disable +#nullable enable using System; using System.Collections.Generic; +using System.IO; using System.Text; using Microsoft.Build.Utilities; -using Xamarin.Android.Tasks.LLVMIR; +using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks { @@ -46,289 +47,244 @@ public JniRemappingMethodReplacement (string sourceType, string sourceMethod, st } } - class JniRemappingAssemblyGenerator : LlvmIrComposer + class JniRemappingAssemblyGenerator { const string TypeReplacementsVariableName = "jni_remapping_type_replacements"; const string MethodReplacementIndexVariableName = "jni_remapping_method_replacement_index"; - sealed class JniRemappingTypeReplacementEntryContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var entry = EnsureType(data); - - if (MonoAndroidHelper.StringEquals ("name", fieldName)) { - return $" name: {entry.name.str}"; - } - - if (MonoAndroidHelper.StringEquals ("replacement", fieldName)) { - return $" replacement: {entry.replacement}"; - } + // The structure declarations written by this class must be identical to the structures in + // src/native/clr/include/xamarin-app.hh. Data sizes are the sums of sizes of all the + // non-pointer members (see LlvmIrTarget.GetAggregateAlignment), alignments don't include + // the pointer members either (those are accounted for in GetAlignment below) + const ulong JniRemappingIndexMethodEntryDataSize = 9; + const ulong JniRemappingIndexTypeEntryDataSize = 8; + const ulong JniRemappingTypeReplacementEntryDataSize = 4; + const ulong NonPointerMemberAlignment = 4; - return String.Empty; - } - } - - sealed class JniRemappingIndexTypeEntryContextDataProvider : NativeAssemblerStructContextDataProvider + sealed class JniRemappingString { - public override string GetComment (object data, string fieldName) - { - var entry = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("name", fieldName)) { - return $" name: {entry.name.str}"; - } - - return String.Empty; - } - - public override string GetPointedToSymbolName (object data, string fieldName) - { - var entry = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("methods", fieldName)) { - return entry.MethodsArraySymbolName; - } - - return base.GetPointedToSymbolName (data, fieldName); - } + public readonly uint length; + public readonly string str; - public override ulong GetBufferSize (object data, string fieldName) + public JniRemappingString (string str) { - var entry = EnsureType (data); - if (MonoAndroidHelper.StringEquals ("methods", fieldName)) { - return (ulong)entry.TypeMethods.Count; - } - - return 0; + this.str = str; + length = String.IsNullOrEmpty (str) ? 0 : (uint)Encoding.UTF8.GetBytes (str).Length; } } - sealed class JniRemappingIndexMethodEntryContextDataProvider : NativeAssemblerStructContextDataProvider + sealed class JniRemappingIndexMethodEntry { - public override string GetComment (object data, string fieldName) - { - var entry = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("name", fieldName)) { - return $" name: {entry.name.str}"; - } + public readonly JniRemappingString name; + public readonly JniRemappingString signature; + public readonly string target_type; + public readonly string target_name; + public readonly bool is_static; - if (MonoAndroidHelper.StringEquals ("replacement", fieldName)) { - return $" replacement: {entry.replacement.target_type}.{entry.replacement.target_name}"; - } - - if (MonoAndroidHelper.StringEquals ("signature", fieldName)) { - if (entry.signature.length == 0) { - return String.Empty; - } - - return $"signature: {entry.signature.str}"; - } - - return String.Empty; + public JniRemappingIndexMethodEntry (JniRemappingMethodReplacement mmr) + { + name = new JniRemappingString (mmr.SourceMethod); + signature = new JniRemappingString (mmr.SourceMethodSignature); + target_type = mmr.TargetType; + target_name = mmr.TargetMethod; + is_static = mmr.TargetIsStatic; } } - sealed class JniRemappingString - { - public uint length; - public string str; - }; - - sealed class JniRemappingReplacementMethod - { - public string target_type; - public string target_name; - public bool is_static; - }; - - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexMethodEntryContextDataProvider))] - sealed class JniRemappingIndexMethodEntry - { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString signature; - - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingReplacementMethod replacement; - }; - - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingIndexTypeEntryContextDataProvider))] sealed class JniRemappingIndexTypeEntry { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - public uint method_count; - - [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - populated during native code generation - public JniRemappingIndexMethodEntry methods; -#pragma warning restore CS0649 + public readonly JniRemappingString name; + public readonly string MethodsArraySymbolName; + public readonly List TypeMethods = new (); - [NativeAssembler (Ignore = true)] - public string MethodsArraySymbolName; - - [NativeAssembler (Ignore = true)] - public List> TypeMethods; - }; + public JniRemappingIndexTypeEntry (string typeName) + { + name = new JniRemappingString (typeName); + MethodsArraySymbolName = $"mm_{typeName.Replace ('/', '_')}"; + } + } - [NativeAssemblerStructContextDataProvider (typeof(JniRemappingTypeReplacementEntryContextDataProvider))] sealed class JniRemappingTypeReplacementEntry { - [NativeAssembler (UsesDataProvider = true)] - public JniRemappingString name; - - [NativeAssembler (UsesDataProvider = true)] - public string replacement; - }; + public readonly JniRemappingString name; + public readonly string replacement; - List typeReplacementsInput; - List methodReplacementsInput; + public JniRemappingTypeReplacementEntry (JniRemappingTypeReplacement mtr) + { + name = new JniRemappingString (mtr.From); + replacement = mtr.To; + } + } - StructureInfo jniRemappingStringStructureInfo; - StructureInfo jniRemappingReplacementMethodStructureInfo; - StructureInfo jniRemappingIndexMethodEntryStructureInfo; - StructureInfo jniRemappingIndexTypeEntryStructureInfo; - StructureInfo jniRemappingTypeReplacementEntryStructureInfo; + readonly List? typeReplacements; + readonly List? methodIndexTypes; public int ReplacementMethodIndexEntryCount { get; private set; } = 0; - public JniRemappingAssemblyGenerator (TaskLoggingHelper log) - : base (log) - {} + /// + /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// + public bool EmitComments { get; set; } - public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List typeReplacements, List methodReplacements) - : base (log) + public JniRemappingAssemblyGenerator (TaskLoggingHelper log) { - this.typeReplacementsInput = typeReplacements ?? throw new ArgumentNullException (nameof (typeReplacements)); - this.methodReplacementsInput = methodReplacements ?? throw new ArgumentNullException (nameof (methodReplacements)); + if (log == null) { + throw new ArgumentNullException (nameof (log)); + } } - (List>? typeReplacements, List>? methodIndexTypes) Init () + public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List typeReplacements, List methodReplacements) + : this (log) { - if (typeReplacementsInput == null) { - return (null, null); + if (typeReplacements == null) { + throw new ArgumentNullException (nameof (typeReplacements)); } - var typeReplacements = new List> (); - foreach (JniRemappingTypeReplacement mtr in typeReplacementsInput) { - var entry = new JniRemappingTypeReplacementEntry { - name = MakeJniRemappingString (mtr.From), - replacement = mtr.To, - }; - - typeReplacements.Add (new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, entry)); + if (methodReplacements == null) { + throw new ArgumentNullException (nameof (methodReplacements)); } - typeReplacements.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); - var methodIndexTypes = new List> (); - var types = new Dictionary> (StringComparer.Ordinal); + this.typeReplacements = new List (); + foreach (JniRemappingTypeReplacement mtr in typeReplacements) { + this.typeReplacements.Add (new JniRemappingTypeReplacementEntry (mtr)); + } + this.typeReplacements.Sort ((JniRemappingTypeReplacementEntry l, JniRemappingTypeReplacementEntry r) => l.name.str.CompareTo (r.name.str)); - foreach (JniRemappingMethodReplacement mmr in methodReplacementsInput) { - if (!types.TryGetValue (mmr.SourceType, out StructureInstance typeEntry)) { - var entry = new JniRemappingIndexTypeEntry { - name = MakeJniRemappingString (mmr.SourceType), - MethodsArraySymbolName = MakeMethodsArrayName (mmr.SourceType), - TypeMethods = new List> (), - }; + methodIndexTypes = new List (); + var types = new Dictionary (StringComparer.Ordinal); - typeEntry = new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, entry); + foreach (JniRemappingMethodReplacement mmr in methodReplacements) { + if (!types.TryGetValue (mmr.SourceType, out JniRemappingIndexTypeEntry? typeEntry)) { + typeEntry = new JniRemappingIndexTypeEntry (mmr.SourceType); methodIndexTypes.Add (typeEntry); types.Add (mmr.SourceType, typeEntry); } - var method = new JniRemappingIndexMethodEntry { - name = MakeJniRemappingString (mmr.SourceMethod), - signature = MakeJniRemappingString (mmr.SourceMethodSignature), - replacement = new JniRemappingReplacementMethod { - target_type = mmr.TargetType, - target_name = mmr.TargetMethod, - is_static = mmr.TargetIsStatic, - }, - }; - - typeEntry.Instance.TypeMethods.Add (new StructureInstance (jniRemappingIndexMethodEntryStructureInfo, method)); + typeEntry.TypeMethods.Add (new JniRemappingIndexMethodEntry (mmr)); } foreach (var kvp in types) { - kvp.Value.Instance.method_count = (uint)kvp.Value.Instance.TypeMethods.Count; - kvp.Value.Instance.TypeMethods.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); + kvp.Value.TypeMethods.Sort ((JniRemappingIndexMethodEntry l, JniRemappingIndexMethodEntry r) => l.name.str.CompareTo (r.name.str)); } - methodIndexTypes.Sort ((StructureInstance l, StructureInstance r) => l.Instance.name.str.CompareTo (r.Instance.name.str)); + methodIndexTypes.Sort ((JniRemappingIndexTypeEntry l, JniRemappingIndexTypeEntry r) => l.name.str.CompareTo (r.name.str)); ReplacementMethodIndexEntryCount = methodIndexTypes.Count; + } - return (typeReplacements, methodIndexTypes); + public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) + { + var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + var strings = new LlvmIrStringPool (); + ulong alignment = GetAlignment (w); - string MakeMethodsArrayName (string typeName) - { - return $"mm_{typeName.Replace ('/', '_')}"; - } + w.WriteHeader (fileName); + w.Write ($$""" - JniRemappingString MakeJniRemappingString (string str) - { - return new JniRemappingString { - length = GetLength (str), - str = str, - }; - } + %struct.JniRemappingIndexMethodEntry = type { + %struct.JniRemappingString, {{w.Comment (" JniRemappingString name")}} + %struct.JniRemappingString, {{w.Comment (" JniRemappingString signature")}} + %struct.JniRemappingReplacementMethod {{w.Comment (" JniRemappingReplacementMethod replacement")}} + } - uint GetLength (string str) - { - if (String.IsNullOrEmpty (str)) { - return 0; + %struct.JniRemappingIndexTypeEntry = type { + %struct.JniRemappingString, {{w.Comment (" JniRemappingString name")}} + i32, {{w.Comment (" uint32_t method_count")}} + ptr {{w.Comment (" JniRemappingIndexMethodEntry methods")}} } - return (uint)Encoding.UTF8.GetBytes (str).Length; - } - } + %struct.JniRemappingReplacementMethod = type { + ptr, {{w.Comment (" char* target_type")}} + ptr, {{w.Comment (" char* target_name")}} + i1 {{w.Comment (" bool is_static")}} + } - protected override void Construct (LlvmIrModule module) - { - module.DefaultStringGroup = "jremap"; + %struct.JniRemappingString = type { + i32, {{w.Comment (" uint32_t length")}} + ptr {{w.Comment (" char* str")}} + } - MapStructures (module); - List>? typeReplacements; - List>? methodIndexTypes; + %struct.JniRemappingTypeReplacementEntry = type { + %struct.JniRemappingString, {{w.Comment (" JniRemappingString name")}} + ptr {{w.Comment (" char* replacement")}} + } - (typeReplacements, methodIndexTypes) = Init (); + """); + + if (typeReplacements == null || methodIndexTypes == null) { + w.WriteGlobal (TypeReplacementsVariableName, LlvmIrWriter.GlobalConstant, "%struct.JniRemappingTypeReplacementEntry", "zeroinitializer", w.GetAggregateAlignment (alignment, JniRemappingTypeReplacementEntryDataSize)); + w.WriteGlobal (MethodReplacementIndexVariableName, LlvmIrWriter.GlobalConstant, "%struct.JniRemappingIndexTypeEntry", "zeroinitializer", w.GetAggregateAlignment (alignment, JniRemappingIndexTypeEntryDataSize)); + } else { + var elements = new List (typeReplacements.Count); + foreach (JniRemappingTypeReplacementEntry entry in typeReplacements) { + elements.Add ($$""" + %struct.JniRemappingTypeReplacementEntry { + {{RenderString (w, strings, entry.name)}}, {{w.Comment ($" name: {entry.name.str}")}} + ptr {{strings.GetPointer (entry.replacement, "JniRemappingTypeReplacementEntry", "replacement")}}{{w.Comment ($" replacement: {entry.replacement}")}} + } + """); + } + WriteArray (w, TypeReplacementsVariableName, LlvmIrWriter.GlobalConstant, "JniRemappingTypeReplacementEntry", elements, alignment, JniRemappingTypeReplacementEntryDataSize); + + foreach (JniRemappingIndexTypeEntry type in methodIndexTypes) { + elements = new List (type.TypeMethods.Count); + foreach (JniRemappingIndexMethodEntry method in type.TypeMethods) { + string signatureComment = method.signature.length == 0 ? " JniRemappingString signature" : $"signature: {method.signature.str}"; + elements.Add ($$""" + %struct.JniRemappingIndexMethodEntry { + {{RenderString (w, strings, method.name)}}, {{w.Comment ($" name: {method.name.str}")}} + {{RenderString (w, strings, method.signature)}}, {{w.Comment (signatureComment)}} + %struct.JniRemappingReplacementMethod { + ptr {{strings.GetPointer (method.target_type, "JniRemappingReplacementMethod", "target_type")}}, {{w.Comment (" char* target_type")}} + ptr {{strings.GetPointer (method.target_name, "JniRemappingReplacementMethod", "target_name")}}, {{w.Comment (" char* target_name")}} + i1 {{LlvmIrWriter.Bool (method.is_static)}}{{w.Comment (" bool is_static")}} + }{{w.Comment ($" replacement: {method.target_type}.{method.target_name}")}} + } + """); + } + WriteArray (w, type.MethodsArraySymbolName, LlvmIrWriter.LocalConstant, "JniRemappingIndexMethodEntry", elements, alignment, JniRemappingIndexMethodEntryDataSize); + } - if (typeReplacements == null) { - module.AddGlobalVariable ( - typeof(StructureInstance), - TypeReplacementsVariableName, - new StructureInstance (jniRemappingTypeReplacementEntryStructureInfo, new JniRemappingTypeReplacementEntry ()) { IsZeroInitialized = true }, - LlvmIrVariableOptions.GlobalConstant - ); - - module.AddGlobalVariable ( - typeof(StructureInstance), - MethodReplacementIndexVariableName, - new StructureInstance (jniRemappingIndexTypeEntryStructureInfo, new JniRemappingIndexTypeEntry ()) { IsZeroInitialized = true }, - LlvmIrVariableOptions.GlobalConstant - ); - return; + elements = new List (methodIndexTypes.Count); + foreach (JniRemappingIndexTypeEntry type in methodIndexTypes) { + elements.Add ($$""" + %struct.JniRemappingIndexTypeEntry { + {{RenderString (w, strings, type.name)}}, {{w.Comment ($" name: {type.name.str}")}} + i32 {{LlvmIrWriter.Number (type.TypeMethods.Count)}}, {{w.Comment (" uint32_t method_count")}} + ptr @{{type.MethodsArraySymbolName}}{{w.Comment (" JniRemappingIndexMethodEntry* methods")}} + } + """); + } + WriteArray (w, MethodReplacementIndexVariableName, LlvmIrWriter.GlobalConstant, "JniRemappingIndexTypeEntry", elements, alignment, JniRemappingIndexTypeEntryDataSize); } - module.AddGlobalVariable (TypeReplacementsVariableName, typeReplacements, LlvmIrVariableOptions.GlobalConstant); + strings.Write (w); + w.WriteMetadata (); + output.Flush (); + } - foreach (StructureInstance entry in methodIndexTypes) { - module.AddGlobalVariable (entry.Instance.MethodsArraySymbolName, entry.Instance.TypeMethods, LlvmIrVariableOptions.LocalConstant); - } + // All the structures written here either contain pointers or no members with alignment requirements higher than 4 + static ulong GetAlignment (LlvmIrWriter w) => Math.Max (w.Target.PointerSize, NonPointerMemberAlignment); - module.AddGlobalVariable (MethodReplacementIndexVariableName, methodIndexTypes, LlvmIrVariableOptions.GlobalConstant); + // Renders a JniRemappingString structure embedded in another structure, at the second level of indentation + static string RenderString (LlvmIrWriter w, LlvmIrStringPool strings, JniRemappingString s) + { + return $$""" + %struct.JniRemappingString { + i32 {{LlvmIrWriter.Number (s.length)}}, {{w.Comment (" uint32_t length")}} + ptr {{strings.GetPointer (s.str, "JniRemappingString", "str")}}{{w.Comment (" char* str")}} + } + """; } - void MapStructures (LlvmIrModule module) + static void WriteArray (LlvmIrWriter w, string name, string attributes, string structName, List elements, ulong alignment, ulong dataSize) { - jniRemappingStringStructureInfo = module.MapStructure (); - jniRemappingReplacementMethodStructureInfo = module.MapStructure (); - jniRemappingIndexMethodEntryStructureInfo = module.MapStructure (); - jniRemappingIndexTypeEntryStructureInfo = module.MapStructure (); - jniRemappingTypeReplacementEntryStructureInfo = module.MapStructure (); + w.WriteGlobal ( + name, + attributes, + $"[{LlvmIrWriter.Number (elements.Count)} x %struct.{structName}]", + w.ArrayValue (elements, LlvmIrWriter.IndexComment), + w.GetAggregateAlignment (alignment, (ulong)elements.Count * dataSize) + ); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/KnownMetadata.cs b/src/Xamarin.Android.Build.Tasks/Utilities/KnownMetadata.cs index 49bb679683e..9d6d341f2b2 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/KnownMetadata.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/KnownMetadata.cs @@ -5,7 +5,6 @@ static class KnownMetadata { public const string Abi = "Abi"; public const string NativeDontExportSymbols = "DontExportSymbols"; - public const string NativeLinkItemSet = "NativeLinkItemSet"; public const string NativeLinkWholeArchive = "LinkWholeArchive"; public const string NativeSharedLibrary = "NativeSharedLibrary"; public const string RuntimeIdentifier = "RuntimeIdentifier"; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/FunctionAttributes.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/FunctionAttributes.cs deleted file mode 100644 index 8eaaca45e5c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/FunctionAttributes.cs +++ /dev/null @@ -1,1282 +0,0 @@ -using System; -using System.Text; -using System.Globalization; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - // Not all attributes are currently used throughout the code, but we define them call for potential future use. - // Documentation can be found here: https://llvm.org/docs/LangRef.html#function-attributes - /// - /// Abstract base class for LLVM IR function attributes. Function attributes specify properties - /// and optimizations that apply to functions in LLVM IR. - /// See: https://llvm.org/docs/LangRef.html#function-attributes - /// - abstract class LlvmIrFunctionAttribute : IComparable, IComparable, IEquatable - { - /// - /// Gets the name of the function attribute. - /// - public string Name { get; } - /// - /// Gets a value indicating whether the attribute name should be quoted in output. - /// - public bool Quoted { get; } - /// - /// Gets a value indicating whether this attribute supports parameters. - /// - public bool SupportsParams { get; } - /// - /// Gets a value indicating whether parameters are optional for this attribute. - /// - public bool ParamsAreOptional { get; } - /// - /// Gets a value indicating whether this attribute has a value assignment. - /// - public bool HasValueAsignment { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the function attribute. - /// Whether the attribute name should be quoted in output. - /// Whether this attribute supports parameters. - /// Whether parameters are optional for this attribute. - /// Whether this attribute has a value assignment. - /// Thrown when both parameters and value assignment are specified. - protected LlvmIrFunctionAttribute (string name, bool quoted, bool supportsParams, bool optionalParams, bool hasValueAssignment) - { - Name = EnsureNonEmptyParameter (nameof (name), name); - - if (supportsParams && hasValueAssignment) { - throw new InvalidOperationException ($"Function attribute '{name}' cannot have both parameters and an assigned value"); - } - - ParamsAreOptional = optionalParams; - SupportsParams = supportsParams; - HasValueAsignment = hasValueAssignment; - Quoted = quoted; - } - - /// - /// Renders this function attribute to its string representation. - /// - /// The string representation of the function attribute. - public string Render () - { - var sb = new StringBuilder (); - - if (Quoted) { - sb.Append ('"'); - } - - sb.Append (Name); - - if (Quoted) { - sb.Append ('"'); - } - - if (SupportsParams) { - if (!ParamsAreOptional || HasOptionalParams ()) { - sb.Append ('('); - RenderParams (sb); - sb.Append (')'); - } - } else if (HasValueAsignment) { - sb.Append ('='); - if (Quoted) { - sb.Append ('"'); - } - - var value = new StringBuilder (); - RenderAssignedValue (value); - - // LLVM IR escapes characters as \xx where xx is hexadecimal ASCII code - value.Replace ("\"", "\\22"); - sb.Append (value); - - if (Quoted) { - sb.Append ('"'); - } - - } - - return sb.ToString (); - } - - /// - /// When overridden in a derived class, renders the parameters for this function attribute. - /// - /// The string builder to render parameters to. - protected virtual void RenderParams (StringBuilder sb) - {} - - /// - /// When overridden in a derived class, renders the assigned value for this function attribute. - /// - /// The string builder to render the assigned value to. - protected virtual void RenderAssignedValue (StringBuilder sb) - {} - - /// - /// When overridden in a derived class, determines whether this attribute has optional parameters. - /// - /// true if the attribute has optional parameters; otherwise, false. - protected virtual bool HasOptionalParams () - { - return false; - } - - /// - /// Ensures that a parameter value is not null or empty. - /// - /// The name of the parameter. - /// The value of the parameter. - /// The validated parameter value. - /// Thrown when the parameter name is null or empty. - protected string EnsureNonEmptyParameter (string name, string value) - { - if (String.IsNullOrEmpty (name)) { - throw new ArgumentException ("must not be null or empty", name); - } - - return value; - } - - public int CompareTo (object obj) - { - var attr = obj as LlvmIrFunctionAttribute; - if (obj == null) { - return 1; - } - - return CompareTo (attr); - } - - public int CompareTo (LlvmIrFunctionAttribute? other) - { - return Name.CompareTo (other?.Name); - } - - public override int GetHashCode() - { - int hc = 0; - if (Name != null) { - hc ^= Name.GetHashCode (); - } - - return - hc ^ - Quoted.GetHashCode () ^ - SupportsParams.GetHashCode () ^ - ParamsAreOptional.GetHashCode () ^ - HasValueAsignment.GetHashCode (); - } - - public override bool Equals (object obj) - { - var attr = obj as LlvmIrFunctionAttribute; - if (attr == null) { - return false; - } - - return Equals (attr); - } - - public virtual bool Equals (LlvmIrFunctionAttribute other) - { - if (other == null) { - return false; - } - - if (!MonoAndroidHelper.StringEquals (Name, other.Name)) { - return false; - } - - return - Quoted == other.Quoted && - SupportsParams == other.SupportsParams && - ParamsAreOptional == other.ParamsAreOptional && - HasValueAsignment == other.HasValueAsignment; - } - - public static bool operator > (LlvmIrFunctionAttribute a, LlvmIrFunctionAttribute b) - { - return a.CompareTo (b) > 0; - } - - public static bool operator < (LlvmIrFunctionAttribute a, LlvmIrFunctionAttribute b) - { - return a.CompareTo (b) < 0; - } - - public static bool operator >= (LlvmIrFunctionAttribute a, LlvmIrFunctionAttribute b) - { - return a.CompareTo (b) >= 0; - } - - public static bool operator <= (LlvmIrFunctionAttribute a, LlvmIrFunctionAttribute b) - { - return a.CompareTo (b) <= 0; - } - } - - abstract class LlvmIrFlagFunctionAttribute : LlvmIrFunctionAttribute - { - protected LlvmIrFlagFunctionAttribute (string name, bool quoted = false) - : base (name, quoted, supportsParams: false, optionalParams: false, hasValueAssignment: false) - {} - } - - class AlignstackFunctionAttribute : LlvmIrFunctionAttribute - { - uint alignment; - - public AlignstackFunctionAttribute (uint powerOfTwoAlignment) - : base ("alignstack", quoted: false, supportsParams: true, optionalParams: false, hasValueAssignment: false) - { - if ((powerOfTwoAlignment % 2) != 0) { - throw new ArgumentException ("must be power of two", nameof (powerOfTwoAlignment)); - } - - alignment = powerOfTwoAlignment; - } - - protected override void RenderParams (StringBuilder sb) - { - sb.Append (alignment.ToString (CultureInfo.InvariantCulture)); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as AlignstackFunctionAttribute; - if (attr == null) { - return false; - } - - return alignment == attr.alignment; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ alignment.GetHashCode (); - } - } - - class AllocFamilyFunctionAttribute : LlvmIrFunctionAttribute - { - string family; - - public AllocFamilyFunctionAttribute (string familyName) - : base ("alloc-family", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - family = EnsureNonEmptyParameter (nameof (familyName), familyName); - } - - protected override void RenderAssignedValue (StringBuilder sb) - { - sb.Append (family); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as AllocFamilyFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (family, attr.family); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (family?.GetHashCode () ?? 0); - } - } - - class AllockindFunctionAttribute : LlvmIrFunctionAttribute - { - string kind; - - public AllockindFunctionAttribute (string allocKind) - : base ("allockind", quoted: false, supportsParams: true, optionalParams: false, hasValueAssignment: false) - { - kind = EnsureNonEmptyParameter (nameof (allocKind), allocKind); - } - - protected override void RenderParams (StringBuilder sb) - { - sb.Append ('"'); - sb.Append (kind); - sb.Append ('"'); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as AllockindFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (kind, attr.kind); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (kind?.GetHashCode () ?? 0); - } - } - - class AllocsizeFunctionAttribute : LlvmIrFunctionAttribute - { - uint elementSize; - uint? numberOfElements; - - public AllocsizeFunctionAttribute (uint elementSize, uint? numberOfElements = null) - : base ("allocsize", quoted: false, supportsParams: true, optionalParams: false, hasValueAssignment: false) - { - this.elementSize = elementSize; - this.numberOfElements = numberOfElements; - } - - protected override void RenderParams (StringBuilder sb) - { - sb.Append (elementSize.ToString (CultureInfo.InvariantCulture)); - if (!numberOfElements.HasValue) { - return; - } - - sb.Append (", "); - sb.Append (numberOfElements.Value.ToString (CultureInfo.InvariantCulture)); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as AllocsizeFunctionAttribute; - if (attr == null) { - return false; - } - - return elementSize == attr.elementSize && numberOfElements == attr.numberOfElements; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ elementSize.GetHashCode () ^ (numberOfElements?.GetHashCode () ?? 0); - } - } - - class AlwaysinlineFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public AlwaysinlineFunctionAttribute () - : base ("alwaysinline") - {} - } - - class ArgmemonlyFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ArgmemonlyFunctionAttribute () - : base ("argmemonly") - {} - } - - class BuiltinFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public BuiltinFunctionAttribute () - : base ("builtin") - {} - } - - class ColdFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ColdFunctionAttribute () - : base ("cold") - {} - } - - class ConvergentFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ConvergentFunctionAttribute () - : base ("convergent") - {} - } - - class DisableSanitizerInstrumentationFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public DisableSanitizerInstrumentationFunctionAttribute () - : base ("disable_sanitizer_instrumentation") - {} - } - - class DontcallErrorFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public DontcallErrorFunctionAttribute () - : base ("dontcall-error", quoted: true) - {} - } - - class DontcallWarnFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public DontcallWarnFunctionAttribute () - : base ("dontcall-warn", quoted: true) - {} - } - - class FramePointerFunctionAttribute : LlvmIrFunctionAttribute - { - string fpMode; - - public FramePointerFunctionAttribute (string fpMode = "none") - : base ("frame-pointer", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - switch (fpMode) { - case "none": - case "non-leaf": - case "all": - this.fpMode = fpMode; - break; - - default: - throw new ArgumentException ($"unsupported mode value '{fpMode}'", nameof (fpMode)); - } - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (fpMode); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as FramePointerFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (fpMode, attr.fpMode); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (fpMode?.GetHashCode () ?? 0); - } - } - - class HotFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public HotFunctionAttribute () - : base ("hot") - {} - } - - class InaccessiblememonlyFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public InaccessiblememonlyFunctionAttribute () - : base ("inaccessiblememonly") - {} - } - - class InaccessiblememOrArgmemonlyFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public InaccessiblememOrArgmemonlyFunctionAttribute () - : base ("inaccessiblemem_or_argmemonly") - {} - } - - class InlinehintFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public InlinehintFunctionAttribute () - : base ("inlinehint") - {} - } - - class JumptableFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public JumptableFunctionAttribute () - : base ("jumptable") - {} - } - - enum MemoryAttributeAccessKind - { - None, - Read, - Write, - ReadWrite, - } - - class MemoryFunctionAttribute : LlvmIrFunctionAttribute - { - public MemoryAttributeAccessKind? Default { get; set; } - public MemoryAttributeAccessKind? Argmem { get; set; } - public MemoryAttributeAccessKind? InaccessibleMem { get; set; } - - public MemoryFunctionAttribute () - : base ("memory", quoted: false, supportsParams: true, optionalParams: true, hasValueAssignment: false) - {} - - protected override bool HasOptionalParams () - { - // All of them are optional, but at least one of them must be specified - bool ret = Default.HasValue || Argmem.HasValue || InaccessibleMem.HasValue; - if (!ret) { - throw new InvalidOperationException ("Internal error: at least one access kind must be specified"); - } - - return ret; - } - - protected override void RenderParams (StringBuilder sb) - { - bool haveSomething = false; - - if (Default.HasValue) { - AppendParam (GetAccessKindString (Default.Value)); - } - - if (Argmem.HasValue) { - AppendParam ($"argmem: {GetAccessKindString (Argmem.Value)}"); - } - - if (InaccessibleMem.HasValue) { - AppendParam ($"inaccessiblemem: {GetAccessKindString (InaccessibleMem.Value)}"); - } - - void AppendParam (string text) - { - if (haveSomething) { - sb.Append (", "); - } - sb.Append (text); - haveSomething = true; - } - } - - string GetAccessKindString (MemoryAttributeAccessKind kind) - { - return kind switch { - MemoryAttributeAccessKind.None => "none", - MemoryAttributeAccessKind.Read => "read", - MemoryAttributeAccessKind.Write => "write", - MemoryAttributeAccessKind.ReadWrite => "readwrite", - _ => throw new InvalidOperationException ($"Internal error: unsupported access kind {kind}") - }; - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as MemoryFunctionAttribute; - if (attr == null) { - return false; - } - - return Default == attr.Default && Argmem == attr.Argmem && InaccessibleMem == attr.InaccessibleMem; - } - } - - class MinsizeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public MinsizeFunctionAttribute () - : base ("minsize") - {} - } - - class NakedFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NakedFunctionAttribute () - : base ("naked") - {} - } - - class NoInlineLineTablesFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoInlineLineTablesFunctionAttribute () - : base ("no-inline-line-tables", quoted: true) - {} - } - - class NoJumpTablesFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoJumpTablesFunctionAttribute () - : base ("no-jump-tables") - {} - } - - class NobuiltinFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NobuiltinFunctionAttribute () - : base ("nobuiltin") - {} - } - - class NocallbackFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NocallbackFunctionAttribute () - : base ("nocallback") - {} - } - - class NoduplicateFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoduplicateFunctionAttribute () - : base ("noduplicate") - {} - } - - class NofreeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NofreeFunctionAttribute () - : base ("nofree") - {} - } - - class NoimplicitfloatFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoimplicitfloatFunctionAttribute () - : base ("noimplicitfloat") - {} - } - - class NoinlineFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoinlineFunctionAttribute () - : base ("noinline") - {} - } - - class NomergeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NomergeFunctionAttribute () - : base ("nomerge") - {} - } - - class NonlazybindFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NonlazybindFunctionAttribute () - : base ("nonlazybind") - {} - } - - class NoprofileFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoprofileFunctionAttribute () - : base ("noprofile") - {} - } - - class NoredzoneFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoredzoneFunctionAttribute () - : base ("noredzone") - {} - } - - class IndirectTlsSegRefsFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public IndirectTlsSegRefsFunctionAttribute () - : base ("indirect-tls-seg-refs") - {} - } - - class NoreturnFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoreturnFunctionAttribute () - : base ("noreturn") - {} - } - - class NorecurseFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NorecurseFunctionAttribute () - : base ("norecurse") - {} - } - - class WillreturnFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public WillreturnFunctionAttribute () - : base ("willreturn") - {} - } - - class NosyncFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NosyncFunctionAttribute () - : base ("nosync") - {} - } - - class NounwindFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NounwindFunctionAttribute () - : base ("nounwind") - {} - } - - class NosanitizeBoundsFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NosanitizeBoundsFunctionAttribute () - : base ("nosanitize_bounds") - {} - } - - class NosanitizeCoverageFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NosanitizeCoverageFunctionAttribute () - : base ("nosanitize_coverage") - {} - } - - class NullPointerIsValidFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NullPointerIsValidFunctionAttribute () - : base ("null_pointer_is_valid") - {} - } - - class OptforfuzzingFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public OptforfuzzingFunctionAttribute () - : base ("optforfuzzing") - {} - } - - class OptnoneFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public OptnoneFunctionAttribute () - : base ("optnone") - {} - } - - class OptsizeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public OptsizeFunctionAttribute () - : base ("optsize") - {} - } - - class PatchableFunctionFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public PatchableFunctionFunctionAttribute () - : base ("patchable-function", quoted: true) - {} - } - - class ProbeStackFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ProbeStackFunctionAttribute () - : base ("probe-stack") - {} - } - - class ReadnoneFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ReadnoneFunctionAttribute () - : base ("readnone") - {} - } - - class ReadonlyFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ReadonlyFunctionAttribute () - : base ("readonly") - {} - } - - class StackProbeSizeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public StackProbeSizeFunctionAttribute () - : base ("stack-probe-size", quoted: true) - {} - } - - class NoStackArgProbeFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NoStackArgProbeFunctionAttribute () - : base ("no-stack-arg-probe") - {} - } - - class WriteonlyFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public WriteonlyFunctionAttribute () - : base ("writeonly") - {} - } - - class ReturnsTwiceFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ReturnsTwiceFunctionAttribute () - : base ("returns_twice") - {} - } - - class SafestackFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SafestackFunctionAttribute () - : base ("safestack") - {} - } - - class SanitizeAddressFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SanitizeAddressFunctionAttribute () - : base ("sanitize_address") - {} - } - - class SanitizeMemoryFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SanitizeMemoryFunctionAttribute () - : base ("sanitize_memory") - {} - } - - class SanitizeThreadFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SanitizeThreadFunctionAttribute () - : base ("sanitize_thread") - {} - } - - class SanitizeHwaddressFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SanitizeHwaddressFunctionAttribute () - : base ("sanitize_hwaddress") - {} - } - - class SanitizeMemtagFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SanitizeMemtagFunctionAttribute () - : base ("sanitize_memtag") - {} - } - - class SpeculativeLoadHardeningFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SpeculativeLoadHardeningFunctionAttribute () - : base ("speculative_load_hardening") - {} - } - - class SpeculatableFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SpeculatableFunctionAttribute () - : base ("speculatable") - {} - } - - class SspFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SspFunctionAttribute () - : base ("ssp") - {} - } - - class SspstrongFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SspstrongFunctionAttribute () - : base ("sspstrong") - {} - } - - class SspreqFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public SspreqFunctionAttribute () - : base ("sspreq") - {} - } - - class StrictfpFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public StrictfpFunctionAttribute () - : base ("strictfp") - {} - } - - class DenormalFpMathFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public DenormalFpMathFunctionAttribute () - : base ("denormal-fp-math", quoted: true) - {} - } - - class DenormalFpMathF32FunctionAttribute : LlvmIrFlagFunctionAttribute - { - public DenormalFpMathF32FunctionAttribute () - : base ("denormal-fp-math-f32", quoted: true) - {} - } - - class ThunkFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ThunkFunctionAttribute () - : base ("thunk", quoted: true) - {} - } - - class TlsLoadHoistFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public TlsLoadHoistFunctionAttribute () - : base ("tls-load-hoist") - {} - } - - class UwtableFunctionAttribute : LlvmIrFunctionAttribute - { - bool? isSync; - - public UwtableFunctionAttribute (bool? sync = null) - : base ("uwtable", quoted: false, supportsParams: true, optionalParams: true, hasValueAssignment: false) - { - isSync = sync; - } - - protected override bool HasOptionalParams () => isSync.HasValue; - - protected override void RenderParams (StringBuilder sb) - { - if (!isSync.HasValue) { - throw new InvalidOperationException ("Unable to render parameters, none given"); - } - - sb.Append (isSync.Value ? "sync" : "async"); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as UwtableFunctionAttribute; - if (attr == null) { - return false; - } - - return isSync == attr.isSync; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (isSync?.GetHashCode () ?? 0); - } - } - - class NocfCheckFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public NocfCheckFunctionAttribute () - : base ("nocf_check") - {} - } - - class ShadowcallstackFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public ShadowcallstackFunctionAttribute () - : base ("shadowcallstack") - {} - } - - class MustprogressFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public MustprogressFunctionAttribute () - : base ("mustprogress") - {} - } - - class WarnStackSizeFunctionAttribute : LlvmIrFunctionAttribute - { - uint threshold; - - public WarnStackSizeFunctionAttribute (uint threshold) - : base ("warn-stack-size", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.threshold = threshold; - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (threshold); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as WarnStackSizeFunctionAttribute; - if (attr == null) { - return false; - } - - return threshold == attr.threshold; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ threshold.GetHashCode (); - } - } - - class VscaleRangeFunctionAttribute : LlvmIrFunctionAttribute - { - uint min; - uint? max; - - public VscaleRangeFunctionAttribute (uint min, uint? max = null) - : base ("vscale_range", quoted: false, supportsParams: true, optionalParams: false, hasValueAssignment: false) - { - this.min = min; - this.max = max; - } - - protected override void RenderParams (StringBuilder sb) - { - sb.Append (min.ToString (CultureInfo.InvariantCulture)); - if (!max.HasValue) { - return; - } - - sb.Append (", "); - sb.Append (max.Value.ToString (CultureInfo.InvariantCulture)); - } - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as VscaleRangeFunctionAttribute; - if (attr == null) { - return false; - } - - return min == attr.min && max == attr.max; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ min.GetHashCode () ^ (max?.GetHashCode () ?? 0); - } - } - - class MinLegalVectorWidthFunctionAttribute : LlvmIrFunctionAttribute - { - uint size; - - public MinLegalVectorWidthFunctionAttribute (uint size) - : base ("min-legal-vector-width", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.size = size; - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (size.ToString (CultureInfo.InvariantCulture)); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as MinLegalVectorWidthFunctionAttribute; - if (attr == null) { - return false; - } - - return size == attr.size; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ size.GetHashCode (); - } - } - - class StackProtectorBufferSizeFunctionAttribute : LlvmIrFunctionAttribute - { - uint size; - - public StackProtectorBufferSizeFunctionAttribute (uint size) - : base ("stack-protector-buffer-size", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.size = size; - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (size.ToString (CultureInfo.InvariantCulture)); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as StackProtectorBufferSizeFunctionAttribute; - if (attr == null) { - return false; - } - - return size == attr.size; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ size.GetHashCode (); - } - } - - class TargetCpuFunctionAttribute : LlvmIrFunctionAttribute - { - string cpu; - - public TargetCpuFunctionAttribute (string cpu) - : base ("target-cpu", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.cpu = EnsureNonEmptyParameter (nameof (cpu), cpu); - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (cpu); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as TargetCpuFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (cpu, attr.cpu); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (cpu?.GetHashCode () ?? 0); - } - } - - class TuneCpuFunctionAttribute : LlvmIrFunctionAttribute - { - string cpu; - - public TuneCpuFunctionAttribute (string cpu) - : base ("tune-cpu", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.cpu = EnsureNonEmptyParameter (nameof (cpu), cpu); - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (cpu); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as TuneCpuFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (cpu, attr.cpu); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (cpu?.GetHashCode () ?? 0); - } - } - - class TargetFeaturesFunctionAttribute : LlvmIrFunctionAttribute - { - string features; - - public TargetFeaturesFunctionAttribute (string features) - : base ("target-features", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.features = EnsureNonEmptyParameter (nameof (features), features); - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (features); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as TargetFeaturesFunctionAttribute; - if (attr == null) { - return false; - } - - return MonoAndroidHelper.StringEquals (features, attr.features); - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ (features?.GetHashCode () ?? 0); - } - } - - class NoTrappingMathFunctionAttribute : LlvmIrFunctionAttribute - { - bool yesno; - - public NoTrappingMathFunctionAttribute (bool yesno) - : base ("no-trapping-math", quoted: true, supportsParams: false, optionalParams: false, hasValueAssignment: true) - { - this.yesno = yesno; - } - - protected override void RenderAssignedValue (StringBuilder sb) => sb.Append (yesno.ToString ().ToLowerInvariant ()); - - public override bool Equals (LlvmIrFunctionAttribute other) - { - if (!base.Equals (other)) { - return false; - } - - var attr = other as NoTrappingMathFunctionAttribute; - if (attr == null) { - return false; - } - - return yesno == attr.yesno; - } - - public override int GetHashCode () - { - return base.GetHashCode () ^ yesno.GetHashCode (); - } - } - - class StackrealignFunctionAttribute : LlvmIrFlagFunctionAttribute - { - public StackrealignFunctionAttribute () - : base ("stackrealign", quoted: true) - {} - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrAddressSignificance.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrAddressSignificance.cs deleted file mode 100644 index 7616a39084f..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrAddressSignificance.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Address significance, see https://llvm.org/docs/LangRef.html#global-variables - /// - [Flags] - enum LlvmIrAddressSignificance - { - /// - /// Default address significance. - /// - Default = 1 << 0, - /// - /// Unnamed address significance - the address of the global is not significant. - /// - Unnamed = 1 << 1, - /// - /// Local unnamed address significance - the address is not significant within the module. - /// - LocalUnnamed = 1 << 2, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrArraySection.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrArraySection.cs deleted file mode 100644 index 7ddce37a369..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrArraySection.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Base class for array sections used in sectioned arrays. Provides common functionality -/// for organizing array data into named sections with optional headers. -/// -abstract class LlvmIrArraySectionBase -{ - /// - /// Gets the type of data stored in this array section. - /// - public Type DataType { get; } - /// - /// Gets the list of data objects stored in this section. - /// - public List Data { get; } = []; - /// - /// Gets the optional header comment for this section. - /// - public string? Header { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The type of data that will be stored in this section. - /// Optional header comment for the section. - protected LlvmIrArraySectionBase (Type type, string? header = null) - { - DataType = type; - Header = header; - } - - /// - /// Adds a data object to this section. - /// - /// The data object to add to the section. - protected void Add (object data) => Data.Add (data); -} - -/// -/// Represents a typed array section that stores elements of type . -/// -/// The type of elements stored in this array section. -class LlvmIrArraySection : LlvmIrArraySectionBase -{ - /// - /// Initializes a new instance of the class. - /// - /// Optional header comment for the section. - public LlvmIrArraySection (string? header = null) - : base (typeof(T), header) - {} - - /// - /// Adds a typed data element to this section. - /// - /// The data element to add to the section. - public void Add (T data) => base.Add (data!); -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrBufferManager.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrBufferManager.cs deleted file mode 100644 index 221f763b5dd..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrBufferManager.cs +++ /dev/null @@ -1,108 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -partial class LlvmIrModule -{ - /// - /// Manages allocation and naming of buffer variables for structure members that require - /// preallocated buffer space. This class ensures unique buffer names and tracks - /// buffer allocations across structure instances. - /// - sealed class LlvmIrBufferManager - { - Dictionary counters; - Dictionary> bufferVariableNames; - - /// - /// Initializes a new instance of the class. - /// - public LlvmIrBufferManager () - { - counters = new Dictionary (StringComparer.Ordinal); - bufferVariableNames = new Dictionary> (); - } - - /// - /// Allocates a new buffer with a unique name for the specified structure member. - /// - /// The structure instance that contains the member. - /// The structure member information that requires a buffer. - /// The size of the buffer to allocate. - /// The unique name assigned to the allocated buffer variable. - public string Allocate (StructureInstance structure, StructureMemberInfo smi, ulong size) - { - string baseName = $"_{structure.Info.Name}_{smi.Info.Name}"; - - if (!counters.TryGetValue (baseName, out ulong count)) { - count = 0; - counters.Add (baseName, count); - } else { - count++; - counters[baseName] = count; - } - - return Register (structure, smi, $"{baseName}_{count:x}_{structure.IndexInArray:x}"); - } - - /// - /// Gets the buffer variable name for the specified structure member, if one has been allocated. - /// - /// The structure instance containing the member. - /// The structure member information to look up. - /// The buffer variable name if found; otherwise, null. - public string? GetBufferVariableName (StructureInstance structure, StructureMemberInfo smi) - { - if (bufferVariableNames == null || bufferVariableNames.Count == 0 || structure.Obj == null) { - return null; - } - - if (!bufferVariableNames.TryGetValue (structure.Obj, out Dictionary members)) { - return null; - } - - if (!members.TryGetValue (MakeUniqueMemberId (structure, smi), out string bufferVariableName)) { - return null; - } - - return bufferVariableName; - } - - /// - /// Registers a buffer variable name for the specified structure member. - /// - /// The structure instance containing the member. - /// The structure member information. - /// The buffer variable name to register. - /// The registered buffer variable name. - string Register (StructureInstance structure, StructureMemberInfo smi, string bufferVariableName) - { - if (bufferVariableNames == null) { - bufferVariableNames = new Dictionary> (); - } - - if (structure.Obj == null) { - throw new ArgumentException ("Structure instance object cannot be null", nameof (structure)); - } - - if (!bufferVariableNames.TryGetValue (structure.Obj, out Dictionary members)) { - members = new Dictionary (StringComparer.Ordinal); - bufferVariableNames.Add (structure.Obj, members); - } - - members.Add (MakeUniqueMemberId (structure, smi), bufferVariableName); - return bufferVariableName; - } - - /// - /// Creates a unique member identifier combining the member name and structure array index. - /// - /// The structure instance. - /// The structure member information. - /// A unique identifier for the member within the structure array. - string MakeUniqueMemberId (StructureInstance structure, StructureMemberInfo smi) => $"{smi.Info.Name}_{structure.IndexInArray}"; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallMarkers.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallMarkers.cs deleted file mode 100644 index a9c2fb2bb27..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallMarkers.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Represents call markers for LLVM IR call instructions that control tail call optimization. - /// - enum LlvmIrCallMarker - { - /// - /// No call marker specified. - /// - None, - /// - /// Tail call marker - suggests tail call optimization. - /// - Tail, - /// - /// Must tail call marker - requires tail call optimization. - /// - MustTail, - /// - /// No tail call marker - prevents tail call optimization. - /// - NoTail, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallingConvention.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallingConvention.cs deleted file mode 100644 index c9abcc34a0b..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrCallingConvention.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Function calling convention, see https://llvm.org/docs/LangRef.html#callingconv for more detailed docs. -/// Not all conventions are included in this enumeration, only those we may potentially need. -/// -enum LlvmIrCallingConvention -{ - /// - /// Outputs no keyword, making function use whatever is the compiler's default calling convention. - /// - Default, - - /// - /// The C calling convention (`ccc`) - /// - Ccc, - - /// - /// The fast calling convention (`fastcc`). This calling convention attempts to make calls as fast - /// as possible (e.g. by passing things in registers). - /// - Fastcc, - - /// - /// Tail callable calling convention (`tailcc`). This calling convention ensures that calls in tail - /// position will always be tail call optimized. This calling convention is equivalent to fastcc, - /// except for an additional guarantee that tail calls will be produced whenever possible. - /// - Tailcc, -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrComposer.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrComposer.cs deleted file mode 100644 index bcfecf35df6..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrComposer.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.IO; - -using Microsoft.Build.Utilities; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Abstract base class for composing LLVM IR modules. Provides a framework for constructing and generating LLVM IR code. - /// - abstract class LlvmIrComposer - { - bool constructed; - readonly LlvmIrTypeCache cache = new(); - - protected readonly TaskLoggingHelper Log; - - /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to - /// false; see . - /// - public bool EmitComments { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The task logging helper for logging messages. - /// Thrown when is null. - protected LlvmIrComposer (TaskLoggingHelper log) - { - this.Log = log ?? throw new ArgumentNullException (nameof (log)); - } - - /// - /// When overridden in a derived class, constructs the LLVM IR module by adding the necessary structures, functions, and variables. - /// - /// The LLVM IR module to construct. - protected abstract void Construct (LlvmIrModule module); - - /// - /// Constructs the LLVM IR module by calling the derived class implementation and performing necessary finalization. - /// - /// The constructed LLVM IR module. - public LlvmIrModule Construct () - { - var module = new LlvmIrModule (cache, Log); - Construct (module); - module.AfterConstruction (); - constructed = true; - - return module; - } - - /// - /// Generates LLVM IR code for the specified target architecture and writes it to the output stream. - /// - /// The LLVM IR module to generate code from. - /// The target Android architecture. - /// The stream writer to write the generated LLVM IR code to. - /// The name of the file being generated. - /// Thrown when the module has not been constructed yet. - public void Generate (LlvmIrModule module, AndroidTargetArch arch, StreamWriter output, string fileName) - { - if (!constructed) { - throw new InvalidOperationException ($"Internal error: module not constructed yet. Was Construct () called?"); - } - - LlvmIrGenerator generator = LlvmIrGenerator.Create (arch, fileName); - generator.EmitComments = EmitComments; - generator.Generate (output, module); - output.Flush (); - - CleanupAfterGeneration (arch); - } - - /// - /// Performs any necessary cleanup after code generation for the specified architecture. - /// - /// The target Android architecture that was used for generation. - protected virtual void CleanupAfterGeneration (AndroidTargetArch arch) - {} - - /// - /// Ensures that the specified variable is a global variable and returns it as such. - /// - /// The variable to check and cast. - /// The variable cast as a global variable. - /// Thrown when the variable is not a global variable. - protected LlvmIrGlobalVariable EnsureGlobalVariable (LlvmIrVariable variable) - { - var gv = variable as LlvmIrGlobalVariable; - if (gv == null) { - throw new InvalidOperationException ("Internal error: global variable expected"); - } - - return gv; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrDataLayout.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrDataLayout.cs deleted file mode 100644 index 5f312026256..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrDataLayout.cs +++ /dev/null @@ -1,492 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Abstract base class for LLVM IR data layout fields. Data layout fields specify -/// alignment and size information for various data types in LLVM IR. -/// See: https://llvm.org/docs/LangRef.html#data-layout -/// -abstract class LlvmIrDataLayoutField -{ - /// - /// The separator character used between data layout field components. - /// - public const char Separator = ':'; - - /// - /// Gets the identifier for this data layout field. - /// - public string Id { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier for the data layout field. - /// Thrown when id is null or empty. - protected LlvmIrDataLayoutField (string id) - { - if (String.IsNullOrEmpty (id)) { - throw new ArgumentException (nameof (id), "must not be null or empty"); - } - - Id = id; - } - - /// - /// Renders this data layout field to a string builder. - /// - /// The string builder to render to. - public virtual void Render (StringBuilder sb) - { - sb.Append (Id); - } - - /// - /// Converts an unsigned integer to its string representation using invariant culture. - /// - /// The value to convert. - /// The string representation of the value. - public static string ConvertToString (uint v) - { - return v.ToString (CultureInfo.InvariantCulture); - } - - /// - /// Appends an unsigned integer value to the string builder with optional separator. - /// - /// The string builder to append to. - /// The value to append. - /// Whether to prepend the separator character. - protected void Append (StringBuilder sb, uint v, bool needSeparator = true) - { - if (needSeparator) { - sb.Append (Separator); - } - - sb.Append (ConvertToString (v)); - } - - /// - /// Appends a nullable unsigned integer value to the string builder with optional separator. - /// If the value is null, nothing is appended. - /// - /// The string builder to append to. - /// The nullable value to append. - /// Whether to prepend the separator character. - protected void Append (StringBuilder sb, uint? v, bool needSeparator = true) - { - if (!v.HasValue) { - return; - } - - Append (sb, v.Value, needSeparator); - } -} - -/// -/// Represents pointer size and alignment information in LLVM IR data layout. -/// Specifies the size and alignment of pointers for a given address space. -/// -class LlvmIrDataLayoutPointerSize : LlvmIrDataLayoutField -{ - /// - /// Gets or sets the address space for this pointer specification. If null, applies to address space 0. - /// - public uint? AddressSpace { get; set; } - /// - /// Gets the ABI alignment for pointers in bits. - /// - public uint Abi { get; } - /// - /// Gets the size of pointers in bits. - /// - public uint Size { get; } - /// - /// Gets or sets the preferred alignment for pointers in bits. - /// - public uint? Pref { get; set; } - /// - /// Gets or sets the index size for GEP operations in bits. - /// - public uint? Idx { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The size of pointers in bits. - /// The ABI alignment for pointers in bits. - public LlvmIrDataLayoutPointerSize (uint size, uint abi) - : base ("p") - { - Size = size; - Abi = abi; - } - - /// - /// Renders this pointer specification to the string builder. - /// - /// The string builder to render to. - public override void Render (StringBuilder sb) - { - base.Render (sb); - - if (AddressSpace.HasValue && AddressSpace.Value > 0) { - Append (sb, AddressSpace.Value, needSeparator: false); - } - Append (sb, Size); - Append (sb, Abi); - Append (sb, Pref); - Append (sb, Idx); - } -} - -/// -/// Abstract base class for LLVM IR data layout type alignment specifications. -/// Defines common properties for type alignment including size, ABI alignment, and preferred alignment. -/// -abstract class LlvmIrDataLayoutTypeAlignment : LlvmIrDataLayoutField -{ - /// - /// Gets the size of the type in bits. - /// - public uint Size { get; } - /// - /// Gets the ABI alignment for the type in bits. - /// - public uint Abi { get; } - /// - /// Gets or sets the preferred alignment for the type in bits. - /// - public uint? Pref { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier for the alignment specification. - /// The size of the type in bits. - /// The ABI alignment for the type in bits. - protected LlvmIrDataLayoutTypeAlignment (string id, uint size, uint abi) - : base (id) - { - Size = size; - Abi = abi; - } - - /// - /// Renders this type alignment specification to the string builder. - /// - /// The string builder to render to. - public override void Render (StringBuilder sb) - { - base.Render (sb); - - Append (sb, Size, needSeparator: false); - Append (sb, Abi); - Append (sb, Pref); - } -} - -/// -/// Represents integer type alignment specification for LLVM IR data layout. -/// Specifies alignment requirements for integer types of various sizes. -/// -class LlvmIrDataLayoutIntegerAlignment : LlvmIrDataLayoutTypeAlignment -{ - /// - /// Initializes a new instance of the class. - /// - /// The size of the integer type in bits. - /// The ABI alignment for the integer type in bits. - /// The preferred alignment for the integer type in bits. - /// Thrown when ABI alignment for i8 is not 8. - public LlvmIrDataLayoutIntegerAlignment (uint size, uint abi, uint? pref = null) - : base ("i", size, abi) - { - if (size == 8 && abi != 8) { - throw new ArgumentOutOfRangeException (nameof (abi), "Must equal 8 for i8"); - } - - Pref = pref; - } -} - -/// -/// Represents vector type alignment specification for LLVM IR data layout. -/// Specifies alignment requirements for vector types of various sizes. -/// -class LlvmIrDataLayoutVectorAlignment : LlvmIrDataLayoutTypeAlignment -{ - /// - /// Initializes a new instance of the class. - /// - /// The size of the vector type in bits. - /// The ABI alignment for the vector type in bits. - /// The preferred alignment for the vector type in bits. - public LlvmIrDataLayoutVectorAlignment (uint size, uint abi, uint? pref = null) - : base ("v", size, abi) - { - Pref = pref; - } -} - -/// -/// Represents floating-point type alignment specification for LLVM IR data layout. -/// Specifies alignment requirements for floating-point types of various sizes. -/// -class LlvmIrDataLayoutFloatAlignment : LlvmIrDataLayoutTypeAlignment -{ - /// - /// Initializes a new instance of the class. - /// - /// The size of the floating-point type in bits. - /// The ABI alignment for the floating-point type in bits. - /// The preferred alignment for the floating-point type in bits. - public LlvmIrDataLayoutFloatAlignment (uint size, uint abi, uint? pref = null) - : base ("f", size, abi) - { - Pref = pref; - } -} - -/// -/// Represents aggregate object alignment specification for LLVM IR data layout. -/// Specifies alignment requirements for aggregate objects (structures, arrays, etc.). -/// -class LlvmIrDataLayoutAggregateObjectAlignment : LlvmIrDataLayoutField -{ - /// - /// Gets the ABI alignment for aggregate objects in bits. - /// - public uint Abi { get; } - /// - /// Gets or sets the preferred alignment for aggregate objects in bits. - /// - public uint? Pref { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// The ABI alignment for aggregate objects in bits. - /// The preferred alignment for aggregate objects in bits. - public LlvmIrDataLayoutAggregateObjectAlignment (uint abi, uint? pref = null) - : base ("a") - { - Abi = abi; - Pref = pref; - } - - /// - /// Renders this aggregate object alignment specification to the string builder. - /// - /// The string builder to render to. - public override void Render (StringBuilder sb) - { - base.Render (sb); - - Append (sb, Abi); - Append (sb, Pref); - } -} - -enum LlvmIrDataLayoutFunctionPointerAlignmentType -{ - Independent, - Multiple, -} - -class LlvmIrDataLayoutFunctionPointerAlignment : LlvmIrDataLayoutField -{ - public uint Abi { get; } - public LlvmIrDataLayoutFunctionPointerAlignmentType Type { get; } - - public LlvmIrDataLayoutFunctionPointerAlignment (LlvmIrDataLayoutFunctionPointerAlignmentType type, uint abi) - : base ("F") - { - Type = type; - Abi = abi; - } - - public override void Render (StringBuilder sb) - { - base.Render (sb); - - char type = Type switch { - LlvmIrDataLayoutFunctionPointerAlignmentType.Independent => 'i', - LlvmIrDataLayoutFunctionPointerAlignmentType.Multiple => 'n', - _ => throw new InvalidOperationException ($"Unsupported function pointer alignment type '{Type}'") - }; - sb.Append (type); - Append (sb, Abi, needSeparator: false); - } -} - -enum LlvmIrDataLayoutManglingOption -{ - ELF, - GOFF, - MIPS, - MachO, - WindowsX86COFF, - WindowsCOFF, - XCOFF -} - -class LlvmIrDataLayoutMangling : LlvmIrDataLayoutField -{ - public LlvmIrDataLayoutManglingOption Option { get; } - - public LlvmIrDataLayoutMangling (LlvmIrDataLayoutManglingOption option) - : base ("m") - { - Option = option; - } - - public override void Render (StringBuilder sb) - { - base.Render (sb); - - sb.Append (Separator); - - char opt = Option switch { - LlvmIrDataLayoutManglingOption.ELF => 'e', - LlvmIrDataLayoutManglingOption.GOFF => 'l', - LlvmIrDataLayoutManglingOption.MIPS => 'm', - LlvmIrDataLayoutManglingOption.MachO => 'o', - LlvmIrDataLayoutManglingOption.WindowsX86COFF => 'x', - LlvmIrDataLayoutManglingOption.WindowsCOFF => 'w', - LlvmIrDataLayoutManglingOption.XCOFF => 'a', - _ => throw new InvalidOperationException ($"Unsupported mangling option '{Option}'") - }; - - sb.Append (opt); - } -} - -// See: https://llvm.org/docs/LangRef.html#data-layout -class LlvmIrDataLayout -{ - bool bigEndian; - bool littleEndian = true; - - public bool BigEndian { - get => bigEndian; - set { - bigEndian = value; - littleEndian = !bigEndian; - } - } - - public bool LittleEndian { - get => littleEndian; - set { - littleEndian = value; - bigEndian = !littleEndian; - } - } - - public uint? AllocaAddressSpaceId { get; set; } - public uint? GlobalsAddressSpaceId { get; set; } - public LlvmIrDataLayoutMangling? Mangling { get; set; } - public uint? ProgramAddressSpaceId { get; set; } - public uint? StackAlignment { get; set; } - - public LlvmIrDataLayoutAggregateObjectAlignment? AggregateObjectAlignment { get; set; } - public List? FloatAlignment { get; set; } - public LlvmIrDataLayoutFunctionPointerAlignment? FunctionPointerAlignment { get; set; } - public List? IntegerAlignment { get; set; } - public List? VectorAlignment { get; set; } - public List? PointerSize { get; set; } - - public List? NativeIntegerWidths { get; set; } - public List? NonIntegralPointerTypeAddressSpaces { get; set; } - - public string Render () - { - var sb = new StringBuilder (); - - sb.Append ("target datalayout = \""); - - sb.Append (LittleEndian ? 'e' : 'E'); - - if (Mangling != null) { - sb.Append ('-'); - Mangling.Render (sb); - } - - AppendFieldList (PointerSize); - - if (FunctionPointerAlignment != null) { - sb.Append ('-'); - FunctionPointerAlignment.Render (sb); - } - - AppendFieldList (IntegerAlignment); - AppendFieldList (FloatAlignment); - AppendFieldList (VectorAlignment); - - Append ('P', ProgramAddressSpaceId); - Append ('G', GlobalsAddressSpaceId); - Append ('A', AllocaAddressSpaceId); - - if (AggregateObjectAlignment != null) { - sb.Append ('-'); - AggregateObjectAlignment.Render (sb); - } - - AppendList ("n", NativeIntegerWidths); - AppendList ("ni", NonIntegralPointerTypeAddressSpaces); - Append ('S', StackAlignment); - - sb.Append ('"'); - - return sb.ToString (); - - void AppendFieldList (List? list) where T: LlvmIrDataLayoutField - { - if (list == null || list.Count == 0) { - return; - } - - foreach (LlvmIrDataLayoutField field in list) { - sb.Append ('-'); - field.Render (sb); - } - } - - void AppendList (string id, List? list) - { - if (list == null || list.Count == 0) { - return; - } - - sb.Append ('-'); - sb.Append (id); - - bool first = true; - foreach (uint v in list) { - if (first) { - first = false; - } else { - sb.Append (LlvmIrDataLayoutField.Separator); - } - - sb.Append (LlvmIrDataLayoutField.ConvertToString (v)); - } - } - - void Append (char id, uint? v) - { - if (!v.HasValue) { - return; - } - - sb.Append ('-'); - sb.Append (id); - sb.Append (LlvmIrDataLayoutField.ConvertToString (v.Value)); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunction.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunction.cs deleted file mode 100644 index cdeacd342fb..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunction.cs +++ /dev/null @@ -1,533 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - interface ILlvmIrSavedFunctionParameterState {} - - class LlvmIrFunctionParameter : LlvmIrLocalVariable - { - sealed class SavedParameterState : ILlvmIrSavedFunctionParameterState - { - public readonly LlvmIrFunctionParameter Owner; - - public uint? Align; - public bool? AllocPtr; - public uint? Dereferenceable; - public bool? ImmArg; - public bool? NoCapture; - public bool? NonNull; - public bool? NoUndef; - public bool? ReadNone; - public bool? SignExt; - public bool? ZeroExt; - public bool? WriteOnly; - public bool? IsCplusPlusReference; - public bool IsVarArgs; - - public SavedParameterState (LlvmIrFunctionParameter owner, SavedParameterState? previousState = null) - { - Owner = owner; - if (previousState == null) { - return; - } - - Align = previousState.Align; - AllocPtr = previousState.AllocPtr; - Dereferenceable = previousState.Dereferenceable; - ImmArg = previousState.ImmArg; - NoCapture = previousState.NoCapture; - NonNull = previousState.NonNull; - ReadNone = previousState.ReadNone; - SignExt = previousState.SignExt; - ZeroExt = previousState.ZeroExt; - WriteOnly = previousState.WriteOnly; - IsCplusPlusReference = previousState.IsCplusPlusReference; - IsVarArgs = previousState.IsVarArgs; - } - } - - SavedParameterState state; - - // To save on time, we declare only attributes that are actually used in our generated code. More will be added, as needed. - - /// - /// align(n) attribute, see . - /// As a special case for us, a value of 0 means use the natural target pointer alignment. - /// - public uint? Align { - get => state.Align; - set => state.Align = value; - } - - /// - /// allocptr attribute, see - /// - public bool? AllocPtr { - get => state.AllocPtr; - set => state.AllocPtr = value; - } - - /// - /// dereferenceable(n) attribute, see . - /// As a special case for us, a value of 0 means use the natural target pointer alignment. - /// - public uint? Dereferenceable { - get => state.Dereferenceable; - set => state.Dereferenceable = value; - } - - /// - /// immarg attribute, see - /// - public bool? ImmArg { - get => state.ImmArg; - set => state.ImmArg = value; - } - - /// - /// nocapture attribute, see - /// - public bool? NoCapture { - get => state.NoCapture; - set => state.NoCapture = value; - } - - /// - /// nonnull attribute, see - /// - public bool? NonNull { - get => state.NonNull; - set => state.NonNull = value; - } - - /// - /// noundef attribute, see - /// - public bool? NoUndef { - get => state.NoUndef; - set => state.NoUndef = value; - } - - /// - /// readnone attribute, see - /// - public bool? ReadNone { - get => state.ReadNone; - set => state.ReadNone = value; - } - - /// - /// signext attribute, see - /// - public bool? SignExt { - get => state.SignExt; - set => state.SignExt = value; - } - - /// - /// zeroext attribute, see - /// - public bool? ZeroExt { - get => state.ZeroExt; - set => state.ZeroExt = value; - } - - /// - /// writeonly attribute, see - /// - public bool? WriteOnly { - get => state.WriteOnly; - set => state.WriteOnly = value; - } - /// - /// This serves a purely documentational purpose, when generating comments about types. It describes a parameter that is a C++ reference, something we can't - /// reflect on the managed side. - /// - public bool? IsCplusPlusReference { - get => state.IsCplusPlusReference; - set => state.IsCplusPlusReference = value; - } - - /// - /// Indicates that the argument is a C variable arguments placeholder (`...`) - /// - public bool IsVarArgs { - get => state.IsVarArgs; - set => state.IsVarArgs = value; - } - - public LlvmIrFunctionParameter (Type type, string? name = null) - : base (type, name) - { - NameMatters = false; - state = new SavedParameterState (this); - - // TODO: check why it doesn't work as expected - can't see the flags set in the output - if (type == typeof(sbyte) || type == typeof (short)) { - SignExt = true; - } else if (type == typeof(byte) || type == typeof (ushort)) { - ZeroExt = true; - } - } - - /// - /// Save (opaque) parameter state. This is necessary because we generate code from the same model (module) for different - /// targets. At the same time, function, signature and parameter instances are shared between the different code generation - /// sessions, so we must sure the state as set by the model is properly preserved. NOTE: it does NOT make the code thread-safe! - /// Instances are **still** shared and thus different threads would step on each other's toes should they saved and restored - /// state without synchronization. - /// - public ILlvmIrSavedFunctionParameterState SaveState () - { - SavedParameterState ret = state; - state = new SavedParameterState (this, ret); - return ret; - } - - /// - /// Restore (opaque) state. for more info - /// - public void RestoreState (ILlvmIrSavedFunctionParameterState savedState) - { - var oldState = savedState as SavedParameterState; - if (oldState == null) { - throw new InvalidOperationException ("Internal error: savedState not an instance of ParameterState"); - } - - if (oldState.Owner != this) { - throw new InvalidOperationException ("Internal error: savedState not saved by this instance"); - } - - state = oldState; - } - } - - interface ILlvmIrSavedFunctionSignatureState {} - - class LlvmIrFunctionSignature : IEquatable - { - public sealed class ReturnTypeAttributes - { - public bool? InReg; - public bool? NoUndef; - public bool? SignExt; - public bool? ZeroExt; - public bool? NonNull; - - public ReturnTypeAttributes () - {} - - public ReturnTypeAttributes (ReturnTypeAttributes other) - { - InReg = other.InReg; - NoUndef = other.NoUndef; - NonNull = other.NonNull; - SignExt = other.SignExt; - ZeroExt = other.ZeroExt; - } - } - - sealed class SavedSignatureState : ILlvmIrSavedFunctionSignatureState - { - public readonly LlvmIrFunctionSignature Owner; - public readonly IList ParameterStates; - public readonly ReturnTypeAttributes ReturnAttributes; - - public SavedSignatureState (LlvmIrFunctionSignature owner, IList parameterStates, ReturnTypeAttributes returnAttributes) - { - Owner = owner; - ParameterStates = parameterStates; - ReturnAttributes = returnAttributes; - } - } - - ReturnTypeAttributes returnAttributes; - - public string Name { get; } - public Type ReturnType { get; } - public ReturnTypeAttributes ReturnAttributes => returnAttributes; - public IList Parameters { get; } - - public LlvmIrFunctionSignature (string name, Type returnType, IList? parameters = null, ReturnTypeAttributes? returnAttributes = null) - { - if (String.IsNullOrEmpty (name)) { - throw new ArgumentException ("must not be null or empty", nameof (name)); - } - - Name = name; - ReturnType = returnType; - this.returnAttributes = returnAttributes ?? new ReturnTypeAttributes (); - Parameters = parameters ?? new List (); - } - - /// - /// Create new signature using data from the one, with the exception of name. - /// Useful when there are several functions with different names but identical parameters and return types. - /// - public LlvmIrFunctionSignature (string name, LlvmIrFunctionSignature templateSignature) - : this (name, templateSignature.ReturnType, templateSignature.Parameters) - {} - - /// - /// Save (opaque) signature state. This includes states of all the parameters. - /// for more information. - /// - public ILlvmIrSavedFunctionSignatureState SaveState () - { - var list = new List (); - - foreach (LlvmIrFunctionParameter parameter in Parameters) { - list.Add (parameter.SaveState ()); - } - - var ret = new SavedSignatureState (this, list.AsReadOnly (), returnAttributes); - returnAttributes = new ReturnTypeAttributes (returnAttributes); - return ret; - } - - /// - /// Restore (opaque) signature state. This includes states of all the parameters. - /// for more information. - /// - public void RestoreState (ILlvmIrSavedFunctionSignatureState savedState) - { - var oldState = savedState as SavedSignatureState; - if (oldState == null) { - throw new InvalidOperationException ($"Internal error: savedState not an instance of {nameof(SavedSignatureState)}"); - } - - if (oldState.Owner != this) { - throw new InvalidOperationException ("Internal error: savedState not saved by this instance"); - } - - for (int i = 0; i < oldState.ParameterStates.Count; i++) { - ILlvmIrSavedFunctionParameterState parameterState = oldState.ParameterStates[i]; - Parameters[i].RestoreState (parameterState); - } - returnAttributes = new ReturnTypeAttributes (oldState.ReturnAttributes); - } - - public override int GetHashCode () - { - int hc = - Name.GetHashCode () ^ - Parameters.GetHashCode () ^ - ReturnType.GetHashCode (); - - foreach (LlvmIrFunctionParameter p in Parameters) { - hc ^= p.GetHashCode (); - } - - return hc; - } - - public override bool Equals (object obj) - { - var sig = obj as LlvmIrFunctionSignature; - if (sig == null) { - return false; - } - - return Equals (sig); - } - - public bool Equals (LlvmIrFunctionSignature other) - { - if (other == null) { - return false; - } - - if (Parameters.Count != other.Parameters.Count || - ReturnType != other.ReturnType || - !MonoAndroidHelper.StringEquals (Name, other.Name) - ) { - return false; - } - - for (int i = 0; i < Parameters.Count; i++) { - if (Parameters[i] != other.Parameters[i]) { - return false; - } - } - - return true; - } - } - - interface ILlvmIrSavedFunctionState {} - - /// - /// Describes a native function to be emitted or declared and keeps code emitting state between calls to various generator. - /// methods. - /// - class LlvmIrFunction : IEquatable - { - public class FunctionState - { - // Counter shared by unnamed local variables (including function parameters) and unnamed labels. - ulong unnamedTemporaryCounter = 0; - - // Implicit unnamed label at the start of the function - ulong? startingBlockNumber; - - public ulong StartingBlockNumber { - get { - if (startingBlockNumber.HasValue) { - return startingBlockNumber.Value; - } - - throw new InvalidOperationException ($"Internal error: starting block number not set"); - } - } - - public FunctionState () - {} - - public ulong NextTemporary () - { - ulong ret = unnamedTemporaryCounter++; - return ret; - } - - public void ConfigureStartingBlockNumber () - { - if (startingBlockNumber.HasValue) { - return; - } - - startingBlockNumber = unnamedTemporaryCounter++; - } - } - - sealed class SavedFunctionState : ILlvmIrSavedFunctionState - { - public readonly LlvmIrFunction Owner; - public readonly ILlvmIrSavedFunctionSignatureState SignatureState; - - public SavedFunctionState (LlvmIrFunction owner, ILlvmIrSavedFunctionSignatureState signatureState) - { - Owner = owner; - SignatureState = signatureState; - } - } - - FunctionState functionState; - - public LlvmIrFunctionSignature Signature { get; } - public LlvmIrAddressSignificance AddressSignificance { get; set; } = LlvmIrAddressSignificance.LocalUnnamed; - public LlvmIrFunctionAttributeSet? AttributeSet { get; set; } - public LlvmIrLinkage Linkage { get; set; } = LlvmIrLinkage.Default; - public LlvmIrRuntimePreemption RuntimePreemption { get; set; } = LlvmIrRuntimePreemption.Default; - public LlvmIrVisibility Visibility { get; set; } = LlvmIrVisibility.Default; - public LlvmIrCallingConvention CallingConvention { get; set; } = LlvmIrCallingConvention.Default; - public LlvmIrFunctionBody Body { get; } - public string? Comment { get; set; } - public bool ReturnsValue => Signature.ReturnType != typeof(void); - public bool UsesVarArgs { get; } - - public LlvmIrFunction (LlvmIrFunctionSignature signature, LlvmIrFunctionAttributeSet? attributeSet = null) - { - Signature = signature; - AttributeSet = attributeSet; - - functionState = new FunctionState (); - foreach (LlvmIrFunctionParameter parameter in signature.Parameters) { - if (UsesVarArgs) { - throw new InvalidOperationException ($"Internal error: function '{signature.Name}' uses variable arguments and it has at least one argument following the varargs (...) one. This is not allowed."); - } - - if (parameter.IsVarArgs) { - UsesVarArgs = true; - continue; - } - - if (!String.IsNullOrWhiteSpace (parameter.Name)) { - continue; - } - - parameter.AssignNumber (functionState.NextTemporary ()); - } - functionState.ConfigureStartingBlockNumber (); - - Body = new LlvmIrFunctionBody (this, functionState); - } - - /// - /// Create new function using data from the signature, with the exception of name. - /// Useful when there are several functions with different names but identical parameters and return types. - /// - public LlvmIrFunction (string name, LlvmIrFunctionSignature templateSignature, LlvmIrFunctionAttributeSet? attributeSet = null) - : this (new LlvmIrFunctionSignature (name, templateSignature), attributeSet) - {} - - public LlvmIrFunction (string name, Type returnType, List? parameters = null, LlvmIrFunctionAttributeSet? attributeSet = null) - : this (new LlvmIrFunctionSignature (name, returnType, parameters), attributeSet) - {} - - /// - /// Creates a local variable which, if is null or empty, is assinged the correct - /// name based on a counter local to the function. - /// - public LlvmIrLocalVariable CreateLocalVariable (Type type, string? name = null) - { - var ret = new LlvmIrLocalVariable (type, name); - if (String.IsNullOrEmpty (name)) { - ret.AssignNumber (functionState.NextTemporary ()); - } - - return ret; - } - - /// - /// Save (opaque) function state. This includes signature state. - /// for more information. - /// - public ILlvmIrSavedFunctionState SaveState () - { - return new SavedFunctionState (this, Signature.SaveState ()); - } - - /// - /// Restore (opaque) function state. This includes signature state. - /// for more information. - /// - public void RestoreState (ILlvmIrSavedFunctionState savedState) - { - var oldState = savedState as SavedFunctionState; - if (oldState == null) { - throw new InvalidOperationException ($"Internal error: savedState not an instance of {nameof(SavedFunctionState)}"); - } - - if (oldState.Owner != this) { - throw new InvalidOperationException ("Internal error: savedState not saved by this instance"); - } - - Signature.RestoreState (oldState.SignatureState); - } - - public override int GetHashCode () - { - return Signature.GetHashCode (); - } - - public override bool Equals (object obj) - { - var func = obj as LlvmIrFunction; - if (func == null) { - return false; - } - - return Equals (func); - } - - public bool Equals (LlvmIrFunction other) - { - if (other == null) { - return false; - } - - return Signature == other.Signature; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionAttributeSet.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionAttributeSet.cs deleted file mode 100644 index 35ee7d0f25c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionAttributeSet.cs +++ /dev/null @@ -1,110 +0,0 @@ -#nullable enable -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -class LlvmIrFunctionAttributeSet : IEnumerable, IEquatable -{ - public uint Number { get; set; } = 0; - public bool DoNotAddTargetSpecificAttributes { get; set; } - - HashSet attributes; -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value - conditionally populated based on target architecture - Dictionary>? privateTargetSpecificAttributes; -#pragma warning restore CS0649 - - public LlvmIrFunctionAttributeSet () - { - attributes = new HashSet (); - } - - public LlvmIrFunctionAttributeSet (LlvmIrFunctionAttributeSet other) - { - attributes = new HashSet (other); - Number = other.Number; - } - - public IList? GetPrivateTargetAttributes (AndroidTargetArch targetArch) - { - if (privateTargetSpecificAttributes == null || !privateTargetSpecificAttributes.TryGetValue (targetArch, out List list)) { - return null; - } - - return list.AsReadOnly (); - } - - public void Add (LlvmIrFunctionAttribute attr) - { - if (attr == null) { - throw new ArgumentNullException (nameof (attr)); - } - - if (!attributes.Contains (attr)) { - attributes.Add (attr); - } - } - - public void Add (IList attrList) - { - foreach (LlvmIrFunctionAttribute attr in attrList) { - Add (attr); - } - } - - public string Render () - { - List list = attributes.ToList (); - list.Sort ((LlvmIrFunctionAttribute a, LlvmIrFunctionAttribute b) => a.Name.CompareTo (b.Name)); - - return String.Join (" ", list.Select (a => a.Render ())); - } - - public IEnumerator GetEnumerator () => attributes.GetEnumerator (); - - IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); - - public bool Equals (LlvmIrFunctionAttributeSet? other) - { - if (other == null) { - return false; - } - - if (attributes.Count != other.attributes.Count) { - return false; - } - - foreach (LlvmIrFunctionAttribute attr in attributes) { - if (!other.attributes.Contains (attr)) { - return false; - } - } - - return true; - } - - public override bool Equals (object? obj) - { - var attrSet = obj as LlvmIrFunctionAttributeSet; - if (attrSet == null) { - return false; - } - - return Equals (attrSet); - } - - public override int GetHashCode() - { - int hc = 0; - - foreach (LlvmIrFunctionAttribute attr in attributes) { - hc ^= attr?.GetHashCode () ?? 0; - } - - return hc; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionBody.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionBody.cs deleted file mode 100644 index 2a33931edaf..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrFunctionBody.cs +++ /dev/null @@ -1,330 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Abstract class from which all of the items (labels, function parameters, -/// local variables and instructions) derive. -/// -abstract class LlvmIrFunctionBodyItem -{ - /// - /// If an item has this property set to true, it won't be written to output when - /// code is generated. This is used for implicit items that don't need to be part of - /// the generated code (e.g. the starting block label) - /// - public bool SkipInOutput { get; protected set; } - public string? Comment { get; set; } - - /// - /// Whether the item consists of nothing but a comment. Such items produce no output at - /// all when is false, as opposed - /// to items which merely carry a trailing comment alongside real content. - /// - protected virtual bool IsCommentOnly => false; - - public void Write (GeneratorWriteContext context, LlvmIrGenerator generator) - { - if (IsCommentOnly && !context.EmitComments) { - return; - } - - DoWrite (context, generator); - if (context.EmitComments && !String.IsNullOrEmpty (Comment)) { - context.Output.Write (' '); - generator.WriteComment (context, Comment); - } - context.Output.WriteLine (); - } - - protected abstract void DoWrite (GeneratorWriteContext context, LlvmIrGenerator generator); -} - -/// -/// Base class for function labels and local variables (including parameters), which -/// obtain automatic names derived from a shared counter, unless explicitly named. -/// -abstract class LlvmIrFunctionLocalItem : LlvmIrFunctionBodyItem -{ - string? name; - - public string Name { - get { - if (String.IsNullOrEmpty (name)) { - throw new InvalidOperationException ("Internal error: name hasn't been set yet"); - } - return name; - } - - protected set { - if (String.IsNullOrEmpty (value)) { - throw new InvalidOperationException ("Internal error: value must not be null or empty"); - } - name = value; - } - } - - protected LlvmIrFunctionLocalItem (string? name) - { - if (name != null) { - Name = name; - } - } - - protected LlvmIrFunctionLocalItem (LlvmIrFunction.FunctionState state, string? name) - { - if (name != null) { - if (name.Length == 0) { - throw new ArgumentException ("must not be an empty string", nameof (name)); - } - - Name = name; - return; - } - - SetName (state.NextTemporary ()); - } - - protected void SetName (ulong num) - { - Name = num.ToString (CultureInfo.InvariantCulture); - } - - protected bool NameIsSet () => !String.IsNullOrEmpty (name); -} - -class LlvmIrFunctionLabelItem : LlvmIrFunctionLocalItem -{ - /// - /// Labels are a bit peculiar in that they must not have their name set to the automatic value (based on - /// a counter shared with function parameters) at creation time, but only when they are actually added to - /// the function body. The reason is that LLVM IR requires all the unnamed temporaries (function parameters and - /// labels) to be named sequentially, but sometimes a label must be referenced before it is added to the instruction - /// stream, e.g. in the br instruction. On the other hand, it is perfectly fine to assign label a name that - /// isn't an integer at **instantiation** time, which is why we have the parameter here. - /// - public LlvmIrFunctionLabelItem (string? name = null) - : base (name) - {} - - public void WillAddToBody (LlvmIrFunctionBody functionBody, LlvmIrFunction.FunctionState state) - { - if (NameIsSet ()) { - return; - } - - SetName (state.NextTemporary ()); - } - - protected override void DoWrite (GeneratorWriteContext context, LlvmIrGenerator generator) - { - context.DecreaseIndent (); - - context.Output.WriteLine (); - context.Output.Write (context.CurrentIndent); - context.Output.Write (Name); - context.Output.Write (':'); - - context.IncreaseIndent (); - } -} - -class LlvmIrFunctionBodyComment : LlvmIrFunctionBodyItem -{ - public string Text { get; } - - protected override bool IsCommentOnly => true; - - public LlvmIrFunctionBodyComment (string comment) - { - Text = comment; - } - - protected override void DoWrite (GeneratorWriteContext context, LlvmIrGenerator generator) - { - context.Output.Write (context.CurrentIndent); - generator.WriteCommentLine (context, Text); - } -} - -class LlvmIrFunctionBody -{ - sealed class LlvmIrFunctionImplicitStartLabel : LlvmIrFunctionLabelItem - { - public LlvmIrFunctionImplicitStartLabel (ulong num) - { - SetName (num); - SkipInOutput = true; - } - } - - List items; - HashSet definedLabels; - LlvmIrFunction ownerFunction; - LlvmIrFunction.FunctionState functionState; - LlvmIrFunctionLabelItem implicitStartBlock; - - LlvmIrFunctionLabelItem? precedingBlock1; - LlvmIrFunctionLabelItem? precedingBlock2; - LlvmIrFunctionLabelItem? previousLabel; - - public IList Items => items.AsReadOnly (); - public LlvmIrFunctionLabelItem? PrecedingBlock1 => precedingBlock1; - public LlvmIrFunctionLabelItem? PrecedingBlock2 => precedingBlock2; - - public LlvmIrFunctionBody (LlvmIrFunction func, LlvmIrFunction.FunctionState functionState) - { - ownerFunction = func; - this.functionState = functionState; - definedLabels = new HashSet (StringComparer.Ordinal); - items = new List (); - previousLabel = implicitStartBlock = new LlvmIrFunctionImplicitStartLabel (functionState.StartingBlockNumber); - } - - public void Add (LlvmIrFunctionLabelItem label, string? comment = null) - { - label.WillAddToBody (this, functionState); - if (definedLabels.Contains (label.Name)) { - throw new InvalidOperationException ($"Internal error: label with name '{label.Name}' already added to function '{ownerFunction.Signature.Name}' body"); - } - items.Add (label); - definedLabels.Add (label.Name); - - // Rotate preceding blocks - if (precedingBlock2 != null) { - precedingBlock2 = null; - } - - precedingBlock2 = precedingBlock1; - precedingBlock1 = previousLabel; - previousLabel = label; - - if (comment == null) { - var sb = new StringBuilder (" preds = %"); - sb.Append (precedingBlock1.Name); - if (precedingBlock2 != null) { - sb.Append (", %"); - sb.Append (precedingBlock2.Name); - } - comment = sb.ToString (); - } - label.Comment = comment; - } - - public void Add (LlvmIrFunctionBodyItem item) - { - items.Add (item); - } - - public void AddComment (string text) - { - Add (new LlvmIrFunctionBodyComment (text)); - } - - public LlvmIrInstructions.Alloca Alloca (LlvmIrVariable result) - { - var ret = new LlvmIrInstructions.Alloca (result); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Br Br (LlvmIrFunctionLabelItem label) - { - var ret = new LlvmIrInstructions.Br (label); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Br Br (LlvmIrVariable cond, LlvmIrFunctionLabelItem ifTrue, LlvmIrFunctionLabelItem ifFalse) - { - var ret = new LlvmIrInstructions.Br (cond, ifTrue, ifFalse); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Call Call (LlvmIrFunction function, LlvmIrVariable? result = null, ICollection? arguments = null, LlvmIrVariable? funcPointer = null) - { - var ret = new LlvmIrInstructions.Call (function, result, arguments, funcPointer); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Ext Ext (LlvmIrVariable source, Type targetType, LlvmIrVariable result) - { - var ret = new LlvmIrInstructions.Ext (source, targetType, result); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Icmp Icmp (LlvmIrIcmpCond cond, LlvmIrVariable op1, object? op2, LlvmIrVariable result) - { - var ret = new LlvmIrInstructions.Icmp (cond, op1, op2, result); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Load Load (LlvmIrVariable source, LlvmIrVariable result, LlvmIrMetadataItem? tbaa = null) - { - var ret = new LlvmIrInstructions.Load (source, result) { - TBAA = tbaa, - }; - Add (ret); - return ret; - } - - /// - /// Creates the `phi` instruction form we use the most throughout marshal methods generator - one which refers to an if/else block and where - /// **both** value:label pairs are **required**. Parameters and are nullable because, in theory, - /// it is possible that hasn't had the required blocks defined prior to adding the `phi` instruction and, thus, - /// we must check for the possibility here. - /// - public LlvmIrInstructions.Phi Phi (LlvmIrVariable result, LlvmIrVariable val1, LlvmIrFunctionLabelItem? label1, LlvmIrVariable val2, LlvmIrFunctionLabelItem? label2) - { - var ret = new LlvmIrInstructions.Phi (result, val1, label1, val2, label2); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Ret Ret (Type retvalType, object? retval = null) - { - var ret = new LlvmIrInstructions.Ret (retvalType, retval); - Add (ret); - return ret; - } - - public LlvmIrInstructions.Store Store (LlvmIrVariable from, LlvmIrVariable to, LlvmIrMetadataItem? tbaa = null) - { - var ret = new LlvmIrInstructions.Store (from, to) { - TBAA = tbaa, - }; - - Add (ret); - return ret; - } - - /// - /// Stores `null` in the indicated variable - /// - public LlvmIrInstructions.Store Store (LlvmIrVariable to, LlvmIrMetadataItem? tbaa = null) - { - var ret = new LlvmIrInstructions.Store (to) { - TBAA = tbaa, - }; - - Add (ret); - return ret; - } - - public LlvmIrInstructions.Unreachable Unreachable () - { - var ret = new LlvmIrInstructions.Unreachable (); - - Add (ret); - return ret; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.Constants.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.Constants.cs deleted file mode 100644 index c71b7753b59..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.Constants.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - partial class LlvmIrGenerator - { - // https://llvm.org/docs/LangRef.html#linkage-types - static readonly Dictionary llvmLinkage = new Dictionary { - { LlvmIrLinkage.Default, String.Empty }, - { LlvmIrLinkage.Private, "private" }, - { LlvmIrLinkage.Internal, "internal" }, - { LlvmIrLinkage.AvailableExternally, "available_externally" }, - { LlvmIrLinkage.LinkOnce, "linkonce" }, - { LlvmIrLinkage.Weak, "weak" }, - { LlvmIrLinkage.Common, "common" }, - { LlvmIrLinkage.Appending, "appending" }, - { LlvmIrLinkage.ExternWeak, "extern_weak" }, - { LlvmIrLinkage.LinkOnceODR, "linkonce_odr" }, - { LlvmIrLinkage.External, "external" }, - }; - - // https://llvm.org/docs/LangRef.html#runtime-preemption-specifiers - static readonly Dictionary llvmRuntimePreemption = new Dictionary { - { LlvmIrRuntimePreemption.Default, String.Empty }, - { LlvmIrRuntimePreemption.DSOPreemptable, "dso_preemptable" }, - { LlvmIrRuntimePreemption.DSOLocal, "dso_local" }, - }; - - // https://llvm.org/docs/LangRef.html#visibility-styles - static readonly Dictionary llvmVisibility = new Dictionary { - { LlvmIrVisibility.Default, "default" }, - { LlvmIrVisibility.Hidden, "hidden" }, - { LlvmIrVisibility.Protected, "protected" }, - }; - - // https://llvm.org/docs/LangRef.html#global-variables - static readonly Dictionary llvmAddressSignificance = new Dictionary { - { LlvmIrAddressSignificance.Default, String.Empty }, - { LlvmIrAddressSignificance.Unnamed, "unnamed_addr" }, - { LlvmIrAddressSignificance.LocalUnnamed, "local_unnamed_addr" }, - }; - - // https://llvm.org/docs/LangRef.html#global-variables - static readonly Dictionary llvmWritability = new Dictionary { - { LlvmIrWritability.Constant, "constant" }, - { LlvmIrWritability.Writable, "global" }, - }; - - // https://llvm.org/docs/LangRef.html#callingconv - static readonly Dictionary llvmCallingConvention = new () { - { LlvmIrCallingConvention.Ccc, "ccc" }, - { LlvmIrCallingConvention.Fastcc, "fastcc" }, - { LlvmIrCallingConvention.Tailcc, "tailcc" }, - }; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs deleted file mode 100644 index 2c01dc5e28a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ /dev/null @@ -1,1960 +0,0 @@ -#nullable disable - -using System; -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Reflection; -using System.Text; - -using Microsoft.Android.Build.Tasks; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - sealed class GeneratorStructureInstance : StructureInstance - { - public GeneratorStructureInstance (StructureInfo info, object instance) - : base (info, instance) - {} - } - - sealed class GeneratorWriteContext - { - const char IndentChar = '\t'; - - int currentIndentLevel = 0; - - public readonly TextWriter Output; - public readonly LlvmIrModule Module; - public readonly LlvmIrModuleTarget Target; - public readonly LlvmIrMetadataManager MetadataManager; - public readonly LlvmIrTypeCache TypeCache; - public readonly LlvmIrGenerator Generator; - - /// - /// Whether descriptive comments are written to the generated LLVM IR. Comments make the - /// output easier to read, but they can account for the majority of the file's size and - /// have no effect on the code llc produces. - /// - public readonly bool EmitComments; - - public string CurrentIndent { get; private set; } = String.Empty; - public bool InVariableGroup { get; set; } - public LlvmIrVariableNumberFormat NumberFormat { get; set; } = LlvmIrVariableNumberFormat.Default; - - public GeneratorWriteContext (LlvmIrGenerator generator, TextWriter writer, LlvmIrModule module, LlvmIrModuleTarget target, LlvmIrMetadataManager metadataManager, LlvmIrTypeCache cache) - { - Generator = generator; - EmitComments = generator.EmitComments; - Output = writer; - Module = module; - Target = target; - MetadataManager = metadataManager; - TypeCache = cache; - } - - public void IncreaseIndent () - { - currentIndentLevel++; - CurrentIndent = MakeIndentString (); - } - - public void DecreaseIndent () - { - if (currentIndentLevel > 0) { - currentIndentLevel--; - } - CurrentIndent = MakeIndentString (); - } - - string MakeIndentString () => currentIndentLevel > 0 ? new String (IndentChar, currentIndentLevel) : String.Empty; - } - - partial class LlvmIrGenerator - { - sealed class LlvmTypeInfo - { - public readonly bool IsPointer; - public readonly bool IsAggregate; - public readonly bool IsStructure; - public readonly ulong Size; - public readonly ulong MaxFieldAlignment; - - public LlvmTypeInfo (bool isPointer, bool isAggregate, bool isStructure, ulong size, ulong maxFieldAlignment) - { - IsPointer = isPointer; - IsAggregate = isAggregate; - IsStructure = isStructure; - Size = size; - MaxFieldAlignment = maxFieldAlignment; - } - } - - sealed class BasicType - { - public readonly string Name; - public readonly ulong Size; - public readonly bool IsNumeric; - public readonly bool IsUnsigned; - public readonly bool PreferHex; - public readonly string HexFormat; - - public BasicType (string name, ulong size, bool isNumeric = true, bool isUnsigned = false, bool? preferHex = null) - { - Name = name; - Size = size; - IsNumeric = isNumeric; - IsUnsigned = isUnsigned; - - // If hex preference isn't specified, we determine whether the type wants to be represented in - // the hexadecimal notation based on signedness. Unsigned types will be represented in hexadecimal, - // but signed types will remain decimal, as it's easier for humans to see the actual value of the - // variable, given this note from LLVM IR manual: - // - // Note that hexadecimal integers are sign extended from the number of active bits, i.e. the bit width minus the number of leading zeros. So ‘s0x0001’ of type ‘i16’ will be -1, not 1. - // - // See: https://llvm.org/docs/LangRef.html#simple-constants - // - if (preferHex.HasValue) { - PreferHex = preferHex.Value; - } else { - PreferHex = isUnsigned; - } - if (!PreferHex) { - HexFormat = String.Empty; - return; - } - - HexFormat = $"x{size * 2}"; - } - } - - public const string IRPointerType = "ptr"; - - static readonly Dictionary basicTypeMap = new Dictionary { - { typeof (bool), new ("i1", 1, isNumeric: false, isUnsigned: true, preferHex: false) }, - { typeof (byte), new ("i8", 1, isUnsigned: true) }, - { typeof (char), new ("i16", 2, isUnsigned: true, preferHex: false) }, - { typeof (sbyte), new ("i8", 1) }, - { typeof (short), new ("i16", 2) }, - { typeof (ushort), new ("i16", 2, isUnsigned: true) }, - { typeof (int), new ("i32", 4) }, - { typeof (uint), new ("i32", 4, isUnsigned: true) }, - { typeof (long), new ("i64", 8) }, - { typeof (ulong), new ("i64", 8, isUnsigned: true) }, - { typeof (float), new ("float", 4) }, - { typeof (double), new ("double", 8) }, - { typeof (void), new ("void", 0, isNumeric: false, preferHex: false) }, - }; - - public string FilePath { get; } - public string FileName { get; } - - /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to - /// false, since comments can easily account for more than half of the generated - /// file's size while having no effect whatsoever on the object code llc emits. - /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. - /// - public bool EmitComments { get; set; } - - LlvmIrModuleTarget target; - - protected LlvmIrGenerator (string filePath, LlvmIrModuleTarget target) - { - FilePath = Path.GetFullPath (filePath); - FileName = Path.GetFileName (filePath); - this.target = target; - } - - public static LlvmIrGenerator Create (AndroidTargetArch arch, string fileName) - { - return arch switch { - AndroidTargetArch.Arm => new LlvmIrGenerator (fileName, new LlvmIrModuleArmV7a ()), - AndroidTargetArch.Arm64 => new LlvmIrGenerator (fileName, new LlvmIrModuleAArch64 ()), - AndroidTargetArch.X86 => new LlvmIrGenerator (fileName, new LlvmIrModuleX86 ()), - AndroidTargetArch.X86_64 => new LlvmIrGenerator (fileName, new LlvmIrModuleX64 ()), - _ => throw new InvalidOperationException ($"Unsupported Android target ABI {arch}") - }; - } - - public void Generate (TextWriter writer, LlvmIrModule module) - { - LlvmIrMetadataManager metadataManager = module.GetMetadataManagerCopy (); - target.AddTargetSpecificMetadata (metadataManager); - - var context = new GeneratorWriteContext (this, writer, module, target, metadataManager, module.TypeCache); - if (!String.IsNullOrEmpty (FilePath)) { - WriteCommentLine (context, $" ModuleID = '{FileName}'"); - context.Output.WriteLine ($"source_filename = \"{FileName}\""); - } - - context.Output.WriteLine (target.DataLayout.Render ()); - context.Output.WriteLine ($"target triple = \"{target.Triple}\""); - WriteStructureDeclarations (context); - WriteGlobalVariables (context); - WriteFunctions (context); - - // Bottom of the file - WriteStrings (context); - WriteExternalFunctionDeclarations (context); - WriteAttributeSets (context); - WriteMetadata (context); - } - - void WriteStrings (GeneratorWriteContext context) - { - if (context.Module.Strings == null || context.Module.Strings.Count == 0) { - return; - } - - context.Output.WriteLine (); - WriteComment (context, " Strings"); - - foreach (LlvmIrStringGroup group in context.Module.Strings) { - context.Output.WriteLine (); - - if (!String.IsNullOrEmpty (group.Comment)) { - WriteCommentLine (context, group.Comment); - } - - foreach (LlvmIrStringVariable info in group.Strings) { - string s = QuoteString (info, out ulong size); - - if (context.EmitComments && !info.IsConstantStringLiteral) { - WriteCommentLine (context, $" '{info.Value}'"); - } - - WriteGlobalVariableName (context, info); - - // Strings must always be local symbols, global variables will point to them - WriteVariableOptions (context, LlvmIrVariableOptions.LocalString); - context.Output.Write ('['); - context.Output.Write (size.ToString (CultureInfo.InvariantCulture)); - context.Output.Write ($" x {info.IrType}] "); - if (info.IsConstantStringLiteral) { - context.Output.Write ('c'); - } - context.Output.Write (s); - context.Output.Write (", align "); - context.Output.WriteLine (target.GetAggregateAlignment (1, size).ToString (CultureInfo.InvariantCulture)); - } - } - } - - void WriteGlobalVariables (GeneratorWriteContext context) - { - if (context.Module.GlobalVariables == null || context.Module.GlobalVariables.Count == 0) { - return; - } - - foreach (LlvmIrGlobalVariable gv in context.Module.GlobalVariables) { - context.NumberFormat = gv.NumberFormat; - - if (gv is LlvmIrGroupDelimiterVariable groupDelimiter) { - if (context.EmitComments && !context.InVariableGroup && !String.IsNullOrEmpty (groupDelimiter.Comment)) { - context.Output.WriteLine (); - context.Output.Write (context.CurrentIndent); - WriteComment (context, groupDelimiter.Comment); - } - - context.InVariableGroup = !context.InVariableGroup; - if (context.InVariableGroup) { - context.Output.WriteLine (); - } - continue; - } - - if (gv.TargetArch.HasValue && gv.TargetArch.Value != target.TargetArch) { - continue; - } - - if (gv.BeforeWriteCallback != null) { - gv.BeforeWriteCallback (gv, target, gv.BeforeWriteCallbackCallerState); - } - WriteGlobalVariable (context, gv); - } - } - - void WriteGlobalVariableName (GeneratorWriteContext context, LlvmIrGlobalVariable variable) - { - if (!String.IsNullOrEmpty (variable.Comment)) { - WriteCommentLine (context, variable.Comment); - } - context.Output.Write ('@'); - context.Output.Write (variable.Name); - context.Output.Write (" = "); - } - - void WriteVariableOptions (GeneratorWriteContext context, LlvmIrVariableOptions options) - { - WriteLinkage (context, options.Linkage); - WritePreemptionSpecifier (context, options.RuntimePreemption); - WriteVisibility (context, options.Visibility); - WriteAddressSignificance (context, options.AddressSignificance); - WriteWritability (context, options.Writability); - } - - void WriteVariableOptions (GeneratorWriteContext context, LlvmIrGlobalVariable variable, LlvmIrVariableOptions? defaultOptions = null) - { - LlvmIrVariableOptions options = variable.Options ?? defaultOptions ?? LlvmIrGlobalVariable.DefaultOptions; - WriteVariableOptions (context, options); - } - - void WriteVariableReference (GeneratorWriteContext context, LlvmIrVariableReference variable) - { - context.Output.Write (variable.Global ? '@' : '%'); - context.Output.Write (variable.Name); - } - - void WriteGlobalVariableStart (GeneratorWriteContext context, LlvmIrGlobalVariable variable) - { - WriteGlobalVariableName (context, variable); - WriteVariableOptions (context, variable, LlvmIrGlobalVariable.DefaultOptions); - } - - void WriteGlobalVariable (GeneratorWriteContext context, LlvmIrGlobalVariable variable) - { - if (!context.InVariableGroup) { - context.Output.WriteLine (); - } - - WriteGlobalVariableStart (context, variable); - WriteTypeAndValue (context, variable, out LlvmTypeInfo typeInfo); - context.Output.Write (", align "); - - ulong alignment; - if (variable.Alignment.HasValue) { - alignment = variable.Alignment.Value; - } else if (typeInfo.IsAggregate) { - ulong count = GetAggregateValueElementCount (context, variable); - alignment = (ulong)target.GetAggregateAlignment ((int)typeInfo.MaxFieldAlignment, count * typeInfo.Size); - } else if (typeInfo.IsStructure) { - alignment = (ulong)target.GetAggregateAlignment ((int)typeInfo.MaxFieldAlignment, typeInfo.Size); - } else if (typeInfo.IsPointer) { - alignment = target.NativePointerSize; - } else { - alignment = typeInfo.Size; - } - - context.Output.WriteLine (alignment.ToString (CultureInfo.InvariantCulture)); - } - - void WriteTypeAndValue (GeneratorWriteContext context, LlvmIrVariable variable, out LlvmTypeInfo typeInfo) - { - WriteType (context, variable, out typeInfo); - context.Output.Write (' '); - - Type valueType; - if (variable.Value is LlvmIrVariable referencedVariable) { - valueType = referencedVariable.Type; - } else { - valueType = variable.Value?.GetType () ?? variable.Type; - } - - if (variable.Value == null) { - // Order of checks is important here. Aggregates can contain pointer types, in which case typeInfo.IsPointer - // will be `true` and the aggregate would be incorrectly initialized with `null` instead of the correct - // `zeroinitializer` - if (typeInfo.IsAggregate) { - WriteValue (context, valueType, variable); - return; - } - - if (typeInfo.IsPointer) { - context.Output.Write ("null"); - return; - } - - throw new InvalidOperationException ($"Internal error: variable '{variable.Name}'' of type {variable.Type} must not have a null value"); - } - - if (!IsValueAssignableFrom (valueType, variable) && !IsValueAssignableFrom (LlvmIrModule.NameValueArrayType, variable)) { - throw new InvalidOperationException ($"Internal error: variable type '{variable.Type}' is different to its value type, '{valueType}'"); - } - - WriteValue (context, valueType, variable); - } - - bool IsValueAssignableFrom (Type valueType, LlvmIrVariable variable) - { - if (valueType != typeof(string) && valueType != typeof(StringHolder)) { - return valueType.IsAssignableFrom (variable.Type); - } - - return variable.Type == typeof(string) || variable.Type == typeof(StringHolder); - } - - ulong GetAggregateValueElementCount (GeneratorWriteContext context, LlvmIrVariable variable) => GetAggregateValueElementCount (context, variable.Type, variable.Value, variable as LlvmIrGlobalVariable); - - ulong GetAggregateValueElementCount (GeneratorWriteContext context, Type type, object? value, LlvmIrGlobalVariable? globalVariable = null) - { - if (type == typeof(LlvmIrStringBlob)) { - return 1; // String blobs are a collection of bytes - } - - if (type.IsSubclassOf (typeof(LlvmIrSectionedArrayBase))) { - var sectionedArray = value as LlvmIrSectionedArrayBase; - if (sectionedArray == null) { - throw new InvalidOperationException ($"Internal error: null sectioned arrays aren't supported"); - } - return sectionedArray.Count; - } - - if (!type.IsArray ()) { - throw new InvalidOperationException ($"Internal error: unknown type {type} when trying to determine aggregate type element count"); - } - - if (value == null) { - if (globalVariable != null) { - if (globalVariable.ArrayDataProvider != null) { - return globalVariable.ArrayDataProvider.GetTotalDataSize (context.Target); - } - return globalVariable.ArrayItemCount; - } - return 0; - } - - // TODO: use caching here - if (type.ImplementsInterface (typeof(IDictionary))) { - return (uint)((IDictionary)value).Count * 2; - } - - if (type.ImplementsInterface (typeof(ICollection))) { - return (uint)((ICollection)value).Count; - } - - if (type.ImplementsInterface (typeof(ICollection<>))) { - return (ulong)GetCollectionOfTCount (type, value); - } - - throw new InvalidOperationException ($"Internal error: should never get here"); - } - - void WriteType (GeneratorWriteContext context, LlvmIrVariable variable, out LlvmTypeInfo typeInfo) - { - WriteType (context, variable.Type, variable.Value, out typeInfo, variable as LlvmIrGlobalVariable); - } - - void WriteType (GeneratorWriteContext context, StructureInstance si, StructureMemberInfo memberInfo, out LlvmTypeInfo typeInfo) - { - if (memberInfo.IsNativePointer) { - typeInfo = new LlvmTypeInfo ( - isPointer: true, - isAggregate: false, - isStructure: false, - size: target.NativePointerSize, - maxFieldAlignment: target.NativePointerSize - ); - - context.Output.Write (IRPointerType); - return; - } - - if (memberInfo.IsInlineArray) { - WriteArrayType (context, memberInfo.MemberType.GetArrayElementType (), memberInfo.ArrayElements, out typeInfo); - return; - } - - if (memberInfo.IsIRStruct) { - var sim = new GeneratorStructureInstance (context.Module.GetStructureInfo (memberInfo.MemberType), memberInfo.GetValue (si.Obj)); - WriteStructureType (context, sim, out typeInfo); - return; - } - - WriteType (context, memberInfo.MemberType, value: null, out typeInfo); - } - - void WriteStructureType (GeneratorWriteContext context, StructureInstance si, out LlvmTypeInfo typeInfo) - { - ulong alignment = GetStructureMaxFieldAlignment (si.Info); - - typeInfo = new LlvmTypeInfo ( - isPointer: false, - isAggregate: false, - isStructure: true, - size: si.Info.Size, - maxFieldAlignment: alignment - ); - - context.Output.Write ('%'); - context.Output.Write (si.Info.NativeTypeDesignator); - context.Output.Write ('.'); - context.Output.Write (si.Info.Name); - } - - void WriteType (GeneratorWriteContext context, Type type, object? value, out LlvmTypeInfo typeInfo, LlvmIrGlobalVariable? globalVariable = null) - { - // Fast path: a basic scalar type (`byte`, `uint`, ...) can never be a structure instance, - // an array or a string blob, so skip the reflection-heavy probing below. Arrays and string - // blobs write one element at a time, so this runs millions of times for a typical - // application and `Type.IsPrimitive`/`Type.IsArray` dominate the generator otherwise. - if (basicTypeMap.ContainsKey (type)) { - string basicIRType = GetIRType (context, type, out ulong basicSize, out bool basicIsPointer); - typeInfo = new LlvmTypeInfo ( - isPointer: basicIsPointer, - isAggregate: false, - isStructure: false, - size: basicSize, - maxFieldAlignment: basicSize - ); - context.Output.Write (basicIRType); - return; - } - - if (IsStructureInstance (type)) { - if (value == null) { - throw new ArgumentException ($"must not be null for structure instances ({type})", nameof (value)); - } - - WriteStructureType (context, (StructureInstance)value, out typeInfo); - return; - } - - string irType; - ulong size; - bool isPointer; - bool isStandardArray = type.IsArray (); - bool isSectionedArray = !isStandardArray && type.IsSubclassOf (typeof(LlvmIrSectionedArrayBase)); - - if (isStandardArray || isSectionedArray) { - Type elementType; - ulong elementCount; - - if (isStandardArray) { - elementType = type.GetArrayElementType (); - elementCount = GetAggregateValueElementCount (context, type, value, globalVariable); - } else if (isSectionedArray) { - var sectionedArray = value as LlvmIrSectionedArrayBase; - if (sectionedArray == null) { - throw new InvalidOperationException ($"Internal error: sectioned array variables must have a value"); - } - - elementType = sectionedArray.ContainedType; - elementCount = GetAggregateValueElementCount (context, type, value, globalVariable); - } else { - throw new InvalidOperationException ("Internal error: should never get here (forgot to add a handler for another array type?)"); - } - - WriteArrayType (context, elementType, elementCount, globalVariable, out typeInfo); - return; - } - - if (type == typeof(LlvmIrStringBlob)) { - WriteStringBlobType (context, (LlvmIrStringBlob?)value, out typeInfo); - return; - } - - irType = GetIRType (context, type, out size, out isPointer); - typeInfo = new LlvmTypeInfo ( - isPointer: isPointer, - isAggregate: false, - isStructure: false, - size: size, - maxFieldAlignment: size - ); - context.Output.Write (irType); - } - - void WriteStringBlobType (GeneratorWriteContext context, LlvmIrStringBlob? blob, out LlvmTypeInfo typeInfo) - { - long size = blob?.Size ?? 0; - // Blobs are always arrays of bytes - context.Output.Write ($"[{size} x i8]"); - - typeInfo = new LlvmTypeInfo ( - isPointer: false, - isAggregate: true, - isStructure: false, - size: (ulong)size, - maxFieldAlignment: 1 - ); - } - - void WriteArrayType (GeneratorWriteContext context, Type elementType, ulong elementCount, out LlvmTypeInfo typeInfo) - { - WriteArrayType (context, elementType, elementCount, variable: null, out typeInfo); - } - - void WriteArrayType (GeneratorWriteContext context, Type elementType, ulong elementCount, LlvmIrGlobalVariable? variable, out LlvmTypeInfo typeInfo) - { - string irType; - ulong size; - ulong maxFieldAlignment; - bool isPointer; - - if (elementType.IsStructureInstance (out Type? structureType)) { - StructureInfo si = context.Module.GetStructureInfo (structureType); - - irType = $"%{si.NativeTypeDesignator}.{si.Name}"; - size = si.Size; - maxFieldAlignment = GetStructureMaxFieldAlignment (si); - isPointer = false; - } else { - irType = GetIRType (context, elementType, out size, out isPointer); - maxFieldAlignment = size; - - if (elementType.IsArray) { - if (variable == null) { - throw new InvalidOperationException ($"Internal error: array of arrays ({elementType}) requires variable to be defined"); - } - - // For the sake of simpler code, we currently assume that all the element arrays are of the same size, because that's the only scenario - // that we use at this time. - var value = variable.Value as ICollection; - if (value == null) { - throw new InvalidOperationException ($"Internal error: variable '{variable.Name}' of type '{variable.Type}' is required to have a value of type which implements the ICollection interface"); - } - - if (value.Count == 0) { - throw new InvalidOperationException ($"Internal error: variable '{variable.Name}' of type '{variable.Type}' is required to have a value which is a non-empty ICollection"); - } - - Array? firstItem = null; - foreach (object v in value) { - firstItem = (Array)v; - break; - } - - if (firstItem == null) { - throw new InvalidOperationException ($"Internal error: variable '{variable.Name}' of type '{variable.Type}' is required to have a value which is a non-empty ICollection with non-null elements"); - } - - irType = $"[{MonoAndroidHelper.CultureInvariantToString (firstItem.Length)} x {irType}]"; - } - } - typeInfo = new LlvmTypeInfo ( - isPointer: isPointer, - isAggregate: true, - isStructure: false, - size: size, - maxFieldAlignment: maxFieldAlignment - ); - - context.Output.Write ('['); - context.Output.Write (elementCount.ToString (CultureInfo.InvariantCulture)); - context.Output.Write (" x "); - context.Output.Write (irType); - context.Output.Write (']'); - } - - ulong GetStructureMaxFieldAlignment (StructureInfo si) - { - if (si.HasPointers && target.NativePointerSize > si.MaxFieldAlignment) { - return target.NativePointerSize; - } - - return si.MaxFieldAlignment; - } - - bool IsStructureInstance (Type t) => typeof(StructureInstance).IsAssignableFrom (t); - - void WriteValue (GeneratorWriteContext context, Type valueType, LlvmIrVariable variable) - { - if (variable is LlvmIrGlobalVariable globalVariable && globalVariable.ArrayDataProvider != null) { - WriteStreamedArrayValue (context, globalVariable, globalVariable.ArrayDataProvider); - return; - } - - if (variable.Type.IsArray ()) { - bool zeroInitialize = false; - if (variable is LlvmIrGlobalVariable gv) { - zeroInitialize = gv.ZeroInitializeArray || variable.Value == null; - } else { - zeroInitialize = GetAggregateValueElementCount (context, variable) == 0; - } - - if (zeroInitialize) { - context.Output.Write ("zeroinitializer"); - return; - } - - WriteArrayValue (context, variable); - return; - } - - WriteValue (context, valueType, variable.Value); - } - - void AssertArraySize (StructureInstance si, StructureMemberInfo smi, ulong length, ulong expectedLength) - { - if (length == expectedLength) { - return; - } - - throw new InvalidOperationException ($"Invalid array size in field '{smi.Info.Name}' of structure '{si.Info.Name}', expected {expectedLength}, found {length}"); - } - - void WriteInlineArray (GeneratorWriteContext context, byte[] bytes, bool encodeAsASCII) - { - if (encodeAsASCII) { - context.Output.Write ('c'); - context.Output.Write (QuoteUtf8String (bytes, bytes.Length, out _, nullTerminated: false)); - return; - } - - string irType = MapToIRType (typeof(byte), context.TypeCache); - bool first = true; - context.Output.Write ("[ "); - foreach (byte b in bytes) { - if (!first) { - context.Output.Write (", "); - } else { - first = false; - } - - context.Output.Write ($"{irType} u0x{b:x02}"); - } - context.Output.Write (" ]"); - } - - void WriteValue (GeneratorWriteContext context, StructureInstance structInstance, StructureMemberInfo smi, object? value) - { - if (smi.IsNativePointer) { - if (WriteNativePointerValue (context, structInstance, smi, value)) { - return; - } - } - - if (smi.IsInlineArray) { - Array a = (Array)value; - ulong length = smi.ArrayElements == 0 ? (ulong)a.Length : smi.ArrayElements; - - if (smi.MemberType == typeof(byte[])) { - var bytes = (byte[])value; - - // Byte arrays are represented in the same way as strings, without the explicit NUL termination byte - AssertArraySize (structInstance, smi, length, smi.ArrayElements); - WriteInlineArray (context, bytes, encodeAsASCII: false); - return; - } - - throw new NotSupportedException ($"Internal error: inline arrays of type {smi.MemberType} aren't supported at this point. Field {smi.Info.Name} in structure {structInstance.Info.Name}"); - } - - if (smi.IsIRStruct) { - StructureInfo si = context.Module.GetStructureInfo (smi.MemberType); - WriteValue (context, typeof(GeneratorStructureInstance), new GeneratorStructureInstance (si, value)); - return; - } - - if (smi.Info.IsNativePointerToPreallocatedBuffer (context.TypeCache, out _)) { - string bufferVariableName = context.Module.LookupRequiredBufferVariableName (structInstance, smi); - context.Output.Write ('@'); - context.Output.Write (bufferVariableName); - return; - } - - WriteValue (context, smi.MemberType, value, smi.Info.GetStringEncoding (context.TypeCache)); - } - - bool WriteNativePointerValue (GeneratorWriteContext context, StructureInstance si, StructureMemberInfo smi, object? value) - { - // Structure members decorated with the [NativePointer] attribute cannot have a - // value other than `null`, unless they are strings or references to symbols - - if (smi.Info.PointsToSymbol (context.TypeCache, out string? symbolName)) { - if (String.IsNullOrEmpty (symbolName) && smi.Info.UsesDataProvider (context.TypeCache)) { - if (si.Info.DataProvider == null) { - throw new InvalidOperationException ($"Field '{smi.Info.Name}' of structure '{si.Info.Name}' points to a symbol, but symbol name wasn't provided and there's no configured data context provider"); - } - symbolName = si.Info.DataProvider.GetPointedToSymbolName (si.Obj, smi.Info.Name); - } - - if (String.IsNullOrEmpty (symbolName)) { - context.Output.Write ("null"); - } else { - context.Output.Write ('@'); - context.Output.Write (symbolName); - } - return true; - } - - if (smi.MemberType != typeof(string)) { - context.Output.Write ("null"); - return true; - } - - return false; - } - - string ToHex (BasicType basicTypeDesc, Type type, object? value) - { - const char prefixSigned = 's'; - const char prefixUnsigned = 'u'; - - string hex; - if (type == typeof(byte)) { - hex = ((byte)value).ToString (basicTypeDesc.HexFormat, CultureInfo.InvariantCulture); - } else if (type == typeof(ushort)) { - hex = ((ushort)value).ToString (basicTypeDesc.HexFormat, CultureInfo.InvariantCulture); - } else if (type == typeof(uint)) { - hex = ((uint)value).ToString (basicTypeDesc.HexFormat, CultureInfo.InvariantCulture); - } else if (type == typeof(ulong)) { - hex = ((ulong)value).ToString (basicTypeDesc.HexFormat, CultureInfo.InvariantCulture); - } else { - throw new NotImplementedException ($"Conversion to hexadecimal from type {type} is not implemented"); - }; - - return $"{(basicTypeDesc.IsUnsigned ? prefixUnsigned : prefixSigned)}0x{hex}"; - } - - public void WriteValue (GeneratorWriteContext context, Type type, object? value, LlvmIrStringEncoding stringEncoding = LlvmIrStringEncoding.UTF8) - { - if (value is LlvmIrVariable variableRef) { - context.Output.Write (variableRef.Reference); - return; - } - - bool isBasic = basicTypeMap.TryGetValue (type, out BasicType basicTypeDesc); - if (isBasic) { - if (basicTypeDesc.IsNumeric) { - bool hex = context.NumberFormat switch { - LlvmIrVariableNumberFormat.Default => basicTypeDesc.PreferHex, - LlvmIrVariableNumberFormat.Decimal => false, - LlvmIrVariableNumberFormat.Hexadecimal => true, - _ => throw new InvalidOperationException ($"Internal error: number format {context.NumberFormat} is unsupported") - }; - - context.Output.Write ( - hex ? ToHex (basicTypeDesc, type, value) : MonoAndroidHelper.CultureInvariantToString (value) - ); - return; - } - - if (type == typeof(bool)) { - context.Output.Write ((bool)value ? "true" : "false"); - return; - } - } - - if (IsStructureInstance (type)) { - WriteStructureValue (context, (StructureInstance?)value); - return; - } - - if (type == typeof(IntPtr)) { - // Pointers can only be `null` or a reference to variable - context.Output.Write ("null"); - return; - } - - if (type == typeof(string) || type == typeof(StringHolder)) { - if (value == null) { - context.Output.Write ("null"); - return; - } - - LlvmIrStringVariable sv = context.Module.LookupRequiredVariableForString (StringHolder.AsHolder (value, stringEncoding)); - context.Output.Write (sv.Reference); - return; - } - - if (type == typeof(LlvmIrStringBlob)) { - WriteStringBlobArray (context, (LlvmIrStringBlob)value); - return; - } - - if (type.IsArray) { - if (type == typeof(byte[])) { - WriteInlineArray (context, (byte[])value, encodeAsASCII: true); - return; - } - - throw new NotSupportedException ($"Internal error: array of type {type} is unsupported"); - } - - if (type.IsSubclassOf (typeof(LlvmIrSectionedArrayBase))) { - WriteSectionedArrayValue (context, (LlvmIrSectionedArrayBase)value); - return; - } - - if (type == typeof (LlvmIrVariableReference) || type.IsSubclassOf (typeof (LlvmIrVariableReference))) { - WriteVariableReference (context, (LlvmIrVariableReference)value); - return; - } - - throw new NotSupportedException ($"Internal error: value type '{type}' is unsupported"); - } - - void WriteSectionedArrayValue (GeneratorWriteContext context, LlvmIrSectionedArrayBase? array) - { - if (array == null) { - context.Output.Write (" zeroinitializer"); - return; - } - - Type elementType = array.ContainedType; - WriteArrayValueStart (context); - ulong globalCounter = 0; - int lastSectionIndex = array.Sections.Count - 1; - - for (int i = 0; i < array.Sections.Count; i++) { - LlvmIrArraySectionBase section = array.Sections[i]; - - if (i > 0) { - context.Output.WriteLine (); - } - - if (!String.IsNullOrEmpty (section.Header)) { - context.Output.Write (context.CurrentIndent); - WriteCommentLine (context, $" Module map index: {globalCounter}"); - context.Output.Write (context.CurrentIndent); - WriteCommentLine (context, section.Header); - } - - WriteArrayEntries ( - context, - variable: null, - section.Data, - section.DataType, - stride: 1, - writeIndices: true, - terminateWithComma: i < lastSectionIndex - ); - globalCounter += (ulong)section.Data.Count; - } - - WriteArrayValueEnd (context); - } - - void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) - { - // Emitted as a single `c"..."` literal rather than one `i8` element per byte, which shrinks - // the `.ll` ~10x. No per-string comments are possible, as the literal must be on one line. - const int HexDigits = 2; - const int MaxEscapeWidth = 1 + HexDigits; - const int ChunkSize = 4096; - - char [] chunk = ArrayPool.Shared.Rent (ChunkSize); - int chunkCapacity = chunk.Length; - int chunkUsed = 0; - - try { - context.Output.Write ('c'); - context.Output.Write ('"'); - - foreach (LlvmIrStringBlob.StringInfo si in blob.GetSegments ()) { - foreach (byte b in si.Bytes) { - WriteByte (b); - } - - // Terminating NUL is counted for each string, but not included in its bytes - WriteByte (0); - } - - Flush (); - context.Output.Write ('"'); - } finally { - ArrayPool.Shared.Return (chunk); - } - - void WriteByte (byte b) - { - if (b != (byte) '"' && b != (byte) '\\' && b >= 32 && b < 127) { - if (chunkUsed == chunkCapacity) { - Flush (); - } - chunk [chunkUsed++] = (char) b; - return; - } - - if (chunkUsed + MaxEscapeWidth > chunkCapacity) { - Flush (); - } - - chunk [chunkUsed++] = '\\'; - HexUtilities.WriteHex (chunk.AsSpan (chunkUsed, HexDigits), b, upperCase: true); - chunkUsed += HexDigits; - } - - void Flush () - { - if (chunkUsed == 0) { - return; - } - - context.Output.Write (chunk, 0, chunkUsed); - chunkUsed = 0; - } - } - - void WriteStructureValue (GeneratorWriteContext context, StructureInstance? instance) - { - if (instance == null || instance.IsZeroInitialized) { - context.Output.Write ("zeroinitializer"); - return; - } - - context.Output.WriteLine ('{'); - context.IncreaseIndent (); - - StructureInfo info = instance.Info; - int lastMember = info.Members.Count - 1; - - for (int i = 0; i < info.Members.Count; i++) { - StructureMemberInfo smi = info.Members[i]; - if (!smi.IsSupportedForTarget (context.Target)) { - continue; - } - - context.Output.Write (context.CurrentIndent); - WriteType (context, instance, smi, out _); - context.Output.Write (' '); - - object? value = GetTypedMemberValue (context, info, smi, instance, smi.MemberType); - LlvmIrVariableNumberFormat numberFormat = smi.Info.GetNumberFormat (context.TypeCache); - LlvmIrVariableNumberFormat? savedNumberFormat = null; - - if (numberFormat != LlvmIrVariableNumberFormat.Default && numberFormat != context.NumberFormat) { - savedNumberFormat = context.NumberFormat; - context.NumberFormat = numberFormat; - } - - WriteValue (context, instance, smi, value); - - if (savedNumberFormat.HasValue) { - context.NumberFormat = savedNumberFormat.Value; - } - - if (i < lastMember) { - context.Output.Write (", "); - } - - if (context.EmitComments) { - string? comment = info.GetCommentFromProvider (smi, instance); - if (String.IsNullOrEmpty (comment)) { - var sb = new StringBuilder (" "); - sb.Append (MapManagedTypeToNative (context, smi)); - sb.Append (' '); - sb.Append (smi.MappedName); - comment = sb.ToString (); - } - WriteCommentLine (context, comment); - } else { - context.Output.WriteLine (); - } - } - - context.DecreaseIndent (); - context.Output.Write (context.CurrentIndent); - context.Output.Write ('}'); - } - - void WriteArrayValueStart (GeneratorWriteContext context) - { - context.Output.WriteLine ('['); - context.IncreaseIndent (); - } - - void WriteArrayValueEnd (GeneratorWriteContext context) - { - context.DecreaseIndent (); - context.Output.Write (']'); - } - - uint GetArrayStride (LlvmIrVariable variable) - { - if ((variable.WriteOptions & LlvmIrVariableWriteOptions.ArrayFormatInRows) == LlvmIrVariableWriteOptions.ArrayFormatInRows) { - return variable.ArrayStride > 0 ? variable.ArrayStride : 1; - } - - return 1; - } - - void WriteArrayEntries (GeneratorWriteContext context, LlvmIrVariable? variable, ICollection? entries, Type elementType, uint stride, bool writeIndices, bool terminateWithComma = false) - { - bool first = true; - // Comments are skipped when the array is written in rows, as they would break the layout - bool ignoreComments = stride > 1 || !context.EmitComments; - string? prevItemComment = null; - ulong counter = 0; - bool writeStringInComment = !ignoreComments && (elementType == typeof(string) || elementType == typeof(StringHolder)); - - if (entries != null) { - foreach (object entry in entries) { - if (!first) { - context.Output.Write (','); - if (stride == 1 || counter % stride == 0) { - WritePrevItemCommentOrNewline (); - context.Output.Write (context.CurrentIndent); - } else { - context.Output.Write (' '); - } - } else { - context.Output.Write (context.CurrentIndent); - first = false; - } - - if (!ignoreComments) { - prevItemComment = null; - if (variable != null && variable.GetArrayItemCommentCallback != null) { - prevItemComment = variable.GetArrayItemCommentCallback (variable, target, counter, entry, variable.GetArrayItemCommentCallbackCallerState); - } - - if (writeIndices && String.IsNullOrEmpty (prevItemComment)) { - string stringComment = String.Empty; - if (writeStringInComment) { - var holder = StringHolder.AsHolder (entry); - stringComment = $" ('{holder.Data}')"; - } - - prevItemComment = $" {counter}{stringComment}"; - } - } - - counter++; - WriteType (context, elementType, entry, out _); - - context.Output.Write (' '); - WriteValue (context, elementType, entry); - } - } - - if (terminateWithComma) { - if (!ignoreComments) { - context.Output.WriteLine (); // must put comma outside the comment - context.Output.Write (context.CurrentIndent); - } - context.Output.Write (','); - } - WritePrevItemCommentOrNewline (); - - void WritePrevItemCommentOrNewline () - { - if (!ignoreComments && !String.IsNullOrEmpty (prevItemComment)) { - context.Output.Write (' '); - WriteCommentLine (context, prevItemComment); - } else { - context.Output.WriteLine (); - } - } - } - - bool ArrayWantsToWriteIndices (LlvmIrVariable variable) => (variable.WriteOptions & LlvmIrVariableWriteOptions.ArrayWriteIndexComments) == LlvmIrVariableWriteOptions.ArrayWriteIndexComments; - - void WriteStreamedArrayValue (GeneratorWriteContext context, LlvmIrGlobalVariable variable, LlvmIrStreamedArrayDataProvider dataProvider) - { - ulong dataSizeSoFar = 0; - ulong totalDataSize = dataProvider.GetTotalDataSize (context.Target); - bool first = true; - - WriteArrayValueStart (context); - while (true) { - (LlvmIrStreamedArrayDataProviderState state, ICollection? data) = dataProvider.GetData (context.Target); - if (state == LlvmIrStreamedArrayDataProviderState.NextSectionNoData) { - continue; - } - - bool mustHaveData = state != LlvmIrStreamedArrayDataProviderState.LastSectionNoData; - if (mustHaveData) { - if (data.Count == 0) { - throw new InvalidOperationException ("Data must be provided for streamed arrays"); - } - - dataSizeSoFar += (ulong)data.Count; - if (dataSizeSoFar > totalDataSize) { - throw new InvalidOperationException ($"Data provider {dataProvider} is trying to write more data than declared"); - } - - if (first) { - first = false; - } else { - context.Output.WriteLine (); - } - string comment = dataProvider.GetSectionStartComment (context.Target); - - if (comment.Length > 0) { - context.Output.Write (context.CurrentIndent); - WriteCommentLine (context, comment); - } - } - - bool lastSection = state == LlvmIrStreamedArrayDataProviderState.LastSection || state == LlvmIrStreamedArrayDataProviderState.LastSectionNoData; - WriteArrayEntries ( - context, - variable, - data, - dataProvider.ArrayElementType, - GetArrayStride (variable), - writeIndices: false, - terminateWithComma: !lastSection - ); - - if (lastSection) { - break; - } - - } - WriteArrayValueEnd (context); - } - - void WriteArrayValue (GeneratorWriteContext context, LlvmIrVariable variable) - { - ICollection entries; - if (variable.Type.ImplementsInterface (typeof(IDictionary))) { - var list = new List (); - foreach (var kvp in (IDictionary)variable.Value) { - list.Add (kvp.Key); - list.Add (kvp.Value); - } - entries = list; - } else if (variable.Type.ImplementsInterface (typeof (ICollection))) { - entries = (ICollection)variable.Value; - } else if (variable.Type.ImplementsInterface (typeof (ICollection<>))) { - // This is slow and messy, but should work for a wide range of types without us having to add - // any explicit support - Type elementType = variable.Type.GetArrayElementType (); - int elementCount = GetCollectionOfTCount (variable.Type, variable.Value); - Array array = Array.CreateInstance (elementType, elementCount); - MethodInfo copyTo = variable.Type.GetMethod ("CopyTo", new Type[] { array.GetType (), typeof (int) }); - copyTo.Invoke (variable.Value, new object[] { array, (int)0 }); - entries = array; - } else { - throw new NotSupportedException ($"Unsupported array value type '{variable.Type}'"); - } - - if (entries.Count == 0) { - context.Output.Write ("zeroinitializer"); - return; - } - - WriteArrayValueStart (context); - - WriteArrayEntries ( - context, - variable, - entries, - variable.Type.GetArrayElementType (), - GetArrayStride (variable), - writeIndices: ArrayWantsToWriteIndices (variable) - ); - - WriteArrayValueEnd (context); - } - - void WriteLinkage (GeneratorWriteContext context, LlvmIrLinkage linkage) - { - if (linkage == LlvmIrLinkage.Default) { - return; - } - - try { - WriteAttribute (context, llvmLinkage[linkage]); - } catch (Exception ex) { - throw new InvalidOperationException ($"Internal error: unsupported writability '{linkage}'", ex); - } - } - - void WriteWritability (GeneratorWriteContext context, LlvmIrWritability writability) - { - try { - WriteAttribute (context, llvmWritability[writability]); - } catch (Exception ex) { - throw new InvalidOperationException ($"Internal error: unsupported writability '{writability}'", ex); - } - } - - void WriteAddressSignificance (GeneratorWriteContext context, LlvmIrAddressSignificance addressSignificance) - { - if (addressSignificance == LlvmIrAddressSignificance.Default) { - return; - } - - try { - WriteAttribute (context, llvmAddressSignificance[addressSignificance]); - } catch (Exception ex) { - throw new InvalidOperationException ($"Internal error: unsupported address significance '{addressSignificance}'", ex); - } - } - - void WriteVisibility (GeneratorWriteContext context, LlvmIrVisibility visibility) - { - if (visibility == LlvmIrVisibility.Default) { - return; - } - - try { - WriteAttribute (context, llvmVisibility[visibility]); - } catch (Exception ex) { - throw new InvalidOperationException ($"Internal error: unsupported visibility '{visibility}'", ex); - } - } - - void WritePreemptionSpecifier (GeneratorWriteContext context, LlvmIrRuntimePreemption preemptionSpecifier) - { - if (preemptionSpecifier == LlvmIrRuntimePreemption.Default) { - return; - } - - try { - WriteAttribute (context, llvmRuntimePreemption[preemptionSpecifier]); - } catch (Exception ex) { - throw new InvalidOperationException ($"Internal error: unsupported preemption specifier '{preemptionSpecifier}'", ex); - } - } - - /// - /// Write attribute named in followed by a single space - /// - void WriteAttribute (GeneratorWriteContext context, string attr) - { - context.Output.Write (attr); - context.Output.Write (' '); - } - - void WriteStructureDeclarations (GeneratorWriteContext context) - { - if (context.Module.Structures == null || context.Module.Structures.Count == 0) { - return; - } - - foreach (StructureInfo si in context.Module.Structures) { - context.Output.WriteLine (); - WriteStructureDeclaration (context, si); - } - } - - void WriteStructureDeclaration (GeneratorWriteContext context, StructureInfo si) - { - // $"%{typeDesignator}.{name} = type " - context.Output.Write ('%'); - context.Output.Write (si.NativeTypeDesignator); - context.Output.Write ('.'); - context.Output.Write (si.Name); - context.Output.Write (" = type "); - - if (si.IsOpaque) { - context.Output.WriteLine ("opaque"); - } else { - context.Output.WriteLine ('{'); - } - - if (si.IsOpaque) { - return; - } - - context.IncreaseIndent (); - for (int i = 0; i < si.Members.Count; i++) { - StructureMemberInfo info = si.Members[i]; - if (!info.IsSupportedForTarget (context.Target)) { - continue; - } - - string nativeType = MapManagedTypeToNative (info.MemberType); - - // TODO: nativeType can be an array, update to indicate that (and get the size) - string arraySize; - if (info.IsNativeArray) { - arraySize = $"[{info.ArrayElements}]"; - } else { - arraySize = String.Empty; - } - - var comment = $" {nativeType} {info.MappedName}{arraySize}"; - WriteStructureDeclarationField (info.IRType, comment, i == si.Members.Count - 1); - } - context.DecreaseIndent (); - - context.Output.WriteLine ('}'); - - void WriteStructureDeclarationField (string typeName, string comment, bool last) - { - context.Output.Write (context.CurrentIndent); - context.Output.Write (typeName); - if (!last) { - context.Output.Write (", "); - } else { - context.Output.Write (' '); - } - - if (context.EmitComments && !String.IsNullOrEmpty (comment)) { - WriteCommentLine (context, comment); - } else { - context.Output.WriteLine (); - } - } - } - - // - // Functions syntax: https://llvm.org/docs/LangRef.html#functions - // - void WriteFunctions (GeneratorWriteContext context) - { - if (context.Module.Functions == null || context.Module.Functions.Count == 0) { - return; - } - - context.Output.WriteLine (); - WriteComment (context, " Functions"); - - foreach (LlvmIrFunction function in context.Module.Functions) { - context.Output.WriteLine (); - WriteFunctionComment (context, function); - - // Must preserve state between calls, different targets may modify function state differently (e.g. set different parameter flags - ILlvmIrSavedFunctionState funcState = WriteFunctionPreamble (context, function, "define"); - WriteFunctionDefinitionLeadingDecorations (context, function); - WriteFunctionSignature (context, function, writeParameterNames: true); - WriteFunctionDefinitionTrailingDecorations (context, function); - WriteFunctionBody (context, function); - function.RestoreState (funcState); - } - } - - void WriteFunctionComment (GeneratorWriteContext context, LlvmIrFunction function) - { - if (String.IsNullOrEmpty (function.Comment)) { - return; - } - - foreach (string commentLine in function.Comment.Split ('\n')) { - context.Output.Write (context.CurrentIndent); - WriteCommentLine (context, commentLine); - } - } - - void WriteFunctionBody (GeneratorWriteContext context, LlvmIrFunction function) - { - context.Output.WriteLine (); - context.Output.WriteLine ('{'); - context.IncreaseIndent (); - - foreach (LlvmIrFunctionBodyItem item in function.Body.Items) { - item.Write (context, this); - } - - context.DecreaseIndent (); - context.Output.WriteLine ('}'); - } - - ILlvmIrSavedFunctionState WriteFunctionPreamble (GeneratorWriteContext context, LlvmIrFunction function, string keyword) - { - ILlvmIrSavedFunctionState funcState = function.SaveState (); - - foreach (LlvmIrFunctionParameter parameter in function.Signature.Parameters) { - target.SetParameterFlags (parameter); - } - - WriteFunctionAttributesComment (context, function); - context.Output.Write (keyword); - context.Output.Write (' '); - - return funcState; - } - - void WriteExternalFunctionDeclarations (GeneratorWriteContext context) - { - if (context.Module.ExternalFunctions == null || context.Module.ExternalFunctions.Count == 0) { - return; - } - - context.Output.WriteLine (); - WriteComment (context, " External functions"); - foreach (LlvmIrFunction function in context.Module.ExternalFunctions) { - context.Output.WriteLine (); - - // Must preserve state between calls, different targets may modify function state differently (e.g. set different parameter flags) - ILlvmIrSavedFunctionState funcState = WriteFunctionPreamble (context, function, "declare"); - WriteFunctionDeclarationLeadingDecorations (context, function); - WriteFunctionSignature (context, function, writeParameterNames: false); - WriteFunctionDeclarationTrailingDecorations (context, function); - - function.RestoreState (funcState); - } - } - - void WriteFunctionAttributesComment (GeneratorWriteContext context, LlvmIrFunction func) - { - if (func.AttributeSet == null) { - return; - } - - if (String.IsNullOrEmpty (func.Comment)) { - context.Output.WriteLine (); - } - WriteCommentLine (context, $" Function attributes: {func.AttributeSet.Render ()}"); - } - - void WriteFunctionDeclarationLeadingDecorations (GeneratorWriteContext context, LlvmIrFunction func) - { - WriteFunctionLeadingDecorations (context, func, declaration: true); - } - - void WriteFunctionDefinitionLeadingDecorations (GeneratorWriteContext context, LlvmIrFunction func) - { - WriteFunctionLeadingDecorations (context, func, declaration: false); - } - - void WriteFunctionLeadingDecorations (GeneratorWriteContext context, LlvmIrFunction func, bool declaration) - { - if (func.Linkage != LlvmIrLinkage.Default) { - context.Output.Write (llvmLinkage[func.Linkage]); - context.Output.Write (' '); - } - - if (!declaration && func.RuntimePreemption != LlvmIrRuntimePreemption.Default) { - context.Output.Write (llvmRuntimePreemption[func.RuntimePreemption]); - context.Output.Write (' '); - } - - if (func.Visibility != LlvmIrVisibility.Default) { - context.Output.Write (llvmVisibility[func.Visibility]); - context.Output.Write (' '); - } - - if (func.CallingConvention != LlvmIrCallingConvention.Default) { - context.Output.Write (llvmCallingConvention[func.CallingConvention]); - context.Output.Write (' '); - } - } - - void WriteFunctionDeclarationTrailingDecorations (GeneratorWriteContext context, LlvmIrFunction func) - { - WriteFunctionTrailingDecorations (context, func, declaration: true); - } - - void WriteFunctionDefinitionTrailingDecorations (GeneratorWriteContext context, LlvmIrFunction func) - { - WriteFunctionTrailingDecorations (context, func, declaration: false); - } - - void WriteFunctionTrailingDecorations (GeneratorWriteContext context, LlvmIrFunction func, bool declaration) - { - if (func.AddressSignificance != LlvmIrAddressSignificance.Default) { - context.Output.Write ($" {llvmAddressSignificance[func.AddressSignificance]}"); - } - - if (func.AttributeSet != null) { - context.Output.Write ($" #{func.AttributeSet.Number}"); - } - } - - public static void WriteReturnAttributes (GeneratorWriteContext context, LlvmIrFunctionSignature.ReturnTypeAttributes returnAttrs) - { - if (AttributeIsSet (returnAttrs.InReg)) { - context.Output.Write ("inreg"); - } - - if (AttributeIsSet (returnAttrs.NoUndef)) { - context.Output.Write ("noundef "); - } - - if (AttributeIsSet (returnAttrs.NonNull)) { - context.Output.Write ("nonnull "); - } - - if (AttributeIsSet (returnAttrs.SignExt)) { - context.Output.Write ("signext "); - } - - if (AttributeIsSet (returnAttrs.ZeroExt)) { - context.Output.Write ("zeroext "); - } - } - - void WriteFunctionSignature (GeneratorWriteContext context, LlvmIrFunction func, bool writeParameterNames) - { - if (func.ReturnsValue) { - WriteReturnAttributes (context, func.Signature.ReturnAttributes); - } - - context.Output.Write (MapToIRType (func.Signature.ReturnType, context.TypeCache)); - context.Output.Write (" @"); - context.Output.Write (func.Signature.Name); - context.Output.Write ('('); - - bool first = true; - bool varargsFound = false; - - foreach (LlvmIrFunctionParameter parameter in func.Signature.Parameters) { - if (varargsFound) { - throw new InvalidOperationException ($"Internal error: function '{func.Signature.Name}' has extra parameters following the C varargs parameter. This is not allowed."); - } - - if (!first) { - context.Output.Write (", "); - } else { - first = false; - } - - if (parameter.IsVarArgs) { - context.Output.Write ("..."); - varargsFound = true; - continue; - } - - context.Output.Write (MapToIRType (parameter.Type, context.TypeCache)); - WriteParameterAttributes (context, parameter); - - if (writeParameterNames) { - if (String.IsNullOrEmpty (parameter.Name)) { - throw new InvalidOperationException ($"Internal error: parameter must have a name"); - } - context.Output.Write (" %"); // Function arguments are always local variables - context.Output.Write (parameter.Name); - } - } - - context.Output.Write (')'); - } - - public static void WriteParameterAttributes (GeneratorWriteContext context, LlvmIrFunctionParameter parameter) - { - var attributes = new List (); - if (AttributeIsSet (parameter.ImmArg)) { - attributes.Add ("immarg"); - } - - if (AttributeIsSet (parameter.AllocPtr)) { - attributes.Add ("allocptr"); - } - - if (AttributeIsSet (parameter.NoCapture)) { - attributes.Add ("nocapture"); - } - - if (AttributeIsSet (parameter.NonNull)) { - attributes.Add ("nonnull"); - } - - if (AttributeIsSet (parameter.NoUndef)) { - attributes.Add ("noundef"); - } - - if (AttributeIsSet (parameter.ReadNone)) { - attributes.Add ("readnone"); - } - - if (AttributeIsSet (parameter.SignExt)) { - attributes.Add ("signext"); - } - - if (AttributeIsSet (parameter.ZeroExt)) { - attributes.Add ("zeroext"); - } - - if (AttributeIsSet (parameter.WriteOnly)) { - attributes.Add ("writeonly"); - } - - if (parameter.Align.HasValue) { - attributes.Add ($"align({ValueOrPointerSize (parameter.Align.Value)})"); - } - - if (parameter.Dereferenceable.HasValue) { - attributes.Add ($"dereferenceable({ValueOrPointerSize (parameter.Dereferenceable.Value)})"); - } - - if (attributes.Count == 0) { - return; - } - - context.Output.Write (' '); - context.Output.Write (String.Join (" ", attributes)); - - uint ValueOrPointerSize (uint? value) - { - if (value.Value == 0) { - return context.Target.NativePointerSize; - } - - return value.Value; - } - } - - static bool AttributeIsSet (bool? attr) => attr.HasValue && attr.Value; - - void WriteAttributeSets (GeneratorWriteContext context) - { - if (context.Module.AttributeSets == null || context.Module.AttributeSets.Count == 0) { - return; - } - - context.Output.WriteLine (); - foreach (LlvmIrFunctionAttributeSet attrSet in context.Module.AttributeSets) { - // Must not modify the original set, it is shared with other targets. - var targetSet = new LlvmIrFunctionAttributeSet (attrSet); - if (!attrSet.DoNotAddTargetSpecificAttributes) { - target.AddTargetSpecificAttributes (targetSet); - } - - IList? privateTargetSet = attrSet.GetPrivateTargetAttributes (target.TargetArch); - if (privateTargetSet != null) { - targetSet.Add (privateTargetSet); - } - - context.Output.WriteLine ($"attributes #{targetSet.Number} = {{ {targetSet.Render ()} }}"); - } - } - - void WriteMetadata (GeneratorWriteContext context) - { - if (context.MetadataManager.Items.Count == 0) { - return; - } - - context.Output.WriteLine (); - WriteCommentLine (context, " Metadata"); - foreach (LlvmIrMetadataItem metadata in context.MetadataManager.Items) { - context.Output.WriteLine (metadata.Render ()); - } - } - - public void WriteComment (GeneratorWriteContext context, string comment) - { - if (!context.EmitComments) { - return; - } - - context.Output.Write (';'); - context.Output.Write (comment); - } - - public void WriteCommentLine (GeneratorWriteContext context, string comment) - { - if (!context.EmitComments) { - return; - } - - WriteComment (context, comment); - context.Output.WriteLine (); - } - - static Type GetActualType (Type type) - { - // Arrays of types are handled elsewhere, so we obtain the array base type here - if (type.IsArray) { - return type.GetElementType (); - } - - return type; - } - - /// - /// Map a managed to its C++ counterpart. Only primitive types, - /// string and IntPtr are supported. - /// - public static string MapManagedTypeToNative (Type type) - { - Type baseType = GetActualType (type); - - if (baseType == typeof (bool)) return "bool"; - if (baseType == typeof (byte)) return "uint8_t"; - if (baseType == typeof (char)) return "char"; - if (baseType == typeof (sbyte)) return "int8_t"; - if (baseType == typeof (short)) return "int16_t"; - if (baseType == typeof (ushort)) return "uint16_t"; - if (baseType == typeof (int)) return "int32_t"; - if (baseType == typeof (uint)) return "uint32_t"; - if (baseType == typeof (long)) return "int64_t"; - if (baseType == typeof (ulong)) return "uint64_t"; - if (baseType == typeof (float)) return "float"; - if (baseType == typeof (double)) return "double"; - if (baseType == typeof (string)) return "char*"; - if (baseType == typeof (IntPtr)) return "void*"; - - return type.GetShortName (); - } - - static string MapManagedTypeToNative (GeneratorWriteContext context, StructureMemberInfo smi) - { - if (smi.Info.IsUnicodeString (context.TypeCache)) { - return "char16_t*"; - } - - string nativeType = MapManagedTypeToNative (smi.MemberType); - // Silly, but effective - if (nativeType[nativeType.Length - 1] == '*') { - return nativeType; - } - - if (!smi.IsNativePointer) { - return nativeType; - } - - return $"{nativeType}*"; - } - - object? GetTypedMemberValue (GeneratorWriteContext context, StructureInfo info, StructureMemberInfo smi, StructureInstance instance, Type expectedType, object? defaultValue = null) - { - object? value = smi.GetValue (instance.Obj); - if (value == null) { - return defaultValue; - } - - Type valueType = value.GetType (); - if (valueType != expectedType) { - throw new InvalidOperationException ($"Field '{smi.Info.Name}' of structure '{info.Name}' should have a value of '{expectedType}' type, instead it had a '{value.GetType ()}'"); - } - - if (valueType == typeof(string) || valueType == typeof(StringHolder)) { - var encoding = smi.Info.GetStringEncoding (context.TypeCache); - return context.Module.LookupRequiredVariableForString (StringHolder.AsHolder (value, encoding)); - } - - return value; - } - - public static string MapToIRType (Type type, LlvmIrTypeCache cache) - { - return MapToIRType (type, cache, out _, out _); - } - - public static string MapToIRType (Type type, LlvmIrTypeCache cache, out ulong size) - { - return MapToIRType (type, cache, out size, out _); - } - - public static string MapToIRType (Type type, LlvmIrTypeCache cache, out bool isPointer) - { - return MapToIRType (type, cache, out _, out isPointer); - } - - /// - /// Maps managed type to equivalent IR type. Puts type size in and whether or not the type - /// is a pointer in . When a type is determined to be a pointer, - /// will be set to 0, because this method doesn't have access to the generator target. In order to adjust pointer - /// size, the instance method must be called (private to the generator as other classes should not - /// have any need to know the pointer size). - /// - public static string MapToIRType (Type type, LlvmIrTypeCache cache, out ulong size, out bool isPointer) - { - type = GetActualType (type); - if (!type.IsNativePointer (cache) && basicTypeMap.TryGetValue (type, out BasicType typeDesc)) { - size = typeDesc.Size; - isPointer = false; - return typeDesc.Name; - } - - // if it's not a basic type, then it's an opaque pointer - size = 0; // Will be determined by the specific target architecture class - isPointer = true; - return IRPointerType; - } - - string GetIRType (GeneratorWriteContext context, Type type, out ulong size, out bool isPointer) - { - string ret = MapToIRType (type, context.TypeCache, out size, out isPointer); - if (isPointer && size == 0) { - size = target.NativePointerSize; - } - - return ret; - } - - public static bool IsFirstClassNonPointerType (Type type) - { - if (type == typeof(void)) { - return false; - } - - return basicTypeMap.ContainsKey (type); - } - - public static string QuoteStringNoEscape (string s) - { - return $"\"{s}\""; - } - - public static string QuoteString (LlvmIrStringVariable variable, out ulong stringSize, bool nullTerminated = true) - { - if (variable.Encoding == LlvmIrStringEncoding.UTF8) { - var value = (StringHolder)variable.Value; - if (value.Data == null) { - throw new InvalidOperationException ("Internal error: null strings not supported here, they should be handled elsewhere."); - } - - int byteCount = Encoding.UTF8.GetByteCount (value.Data); - var bytes = ArrayPool.Shared.Rent (byteCount); - - try { - Encoding.UTF8.GetBytes (value.Data, 0, value.Data.Length, bytes, 0); - return QuoteUtf8String (bytes, byteCount, out stringSize, nullTerminated); - } finally { - ArrayPool.Shared.Return (bytes); - } - } - - if (variable.Encoding == LlvmIrStringEncoding.Unicode) { - return QuoteUnicodeString (variable, out stringSize, nullTerminated); - } - - throw new InvalidOperationException ($"Internal error: unsupported string encoding {variable.Encoding}"); - } - - static string QuoteUnicodeString (LlvmIrStringVariable variable, out ulong stringSize, bool nullTerminated = true) - { - var value = (StringHolder)variable.Value; - if (value.Data == null) { - throw new InvalidOperationException ("Internal error: null strings not supported here, they should be handled elsewhere."); - } - - // Each character/lexeme is encoded as iXY u0xVXYZ + comma and a space, and on top of that we have two square brackets and a trailing nul - var sb = new StringBuilder ((value.Data.Length * 13) + 3); // rough estimate of capacity - sb.Append ('['); - for (int i = 0; i < value.Data.Length; i++) { - var ch = (short)value.Data[i]; - if (i > 0) { - sb.Append (", "); - } - sb.Append ($"{variable.IrType} u0x{ch:X2}"); - } - - if (nullTerminated) { - if (value.Data.Length > 0) { - sb.Append (", "); - } - sb.Append ($"{variable.IrType} 0"); - } - sb.Append (']'); - - stringSize = (ulong)value.Data.Length + (nullTerminated ? 1u : 0u); - return sb.ToString (); - } - - static string QuoteUtf8String (byte[] bytes, int byteCount, out ulong stringSize, bool nullTerminated = true) - { - var sb = new StringBuilder (byteCount * 2); // rough estimate of capacity - - byte b; - for (int i = 0; i < byteCount; i++) { - b = bytes [i]; - if (b != '"' && b != '\\' && b >= 32 && b < 127) { - sb.Append ((char)b); - continue; - } - - sb.Append ('\\'); - sb.Append ($"{b:X2}"); - } - - stringSize = (ulong) byteCount; - if (nullTerminated) { - stringSize++; - sb.Append ("\\00"); - } - - return QuoteStringNoEscape (sb.ToString ()); - } - - static int GetCollectionOfTCount (Type type, object instance) - { - if (!type.ImplementsInterface (typeof (ICollection<>))) { - throw new ArgumentException ("Must implement the ICollection interface", nameof (type)); - } - - PropertyInfo countProperty = type.GetProperty ("Count"); - var ret = (int)countProperty.GetValue (instance); - - if (ret < 0) { - throw new InvalidOperationException ($"Collection count is negative: {ret}"); - } - - return ret; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrIcmpCond.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrIcmpCond.cs deleted file mode 100644 index 63b7602c02d..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrIcmpCond.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Represents comparison conditions for the LLVM IR icmp instruction. - /// - enum LlvmIrIcmpCond - { - /// - /// Equal comparison (eq). - /// - Equal, - /// - /// Not equal comparison (ne). - /// - NotEqual, - /// - /// Unsigned greater than comparison (ugt). - /// - UnsignedGreaterThan, - /// - /// Unsigned greater than or equal comparison (uge). - /// - UnsignedGreaterOrEqual, - /// - /// Unsigned less than comparison (ult). - /// - UnsignedLessThan, - /// - /// Unsigned less than or equal comparison (ule). - /// - UnsignedLessOrEqual, - /// - /// Signed greater than comparison (sgt). - /// - SignedGreaterThan, - /// - /// Signed greater than or equal comparison (sge). - /// - SignedGreaterOrEqual, - /// - /// Signed less than comparison (slt). - /// - SignedLessThan, - /// - /// Signed less than or equal comparison (sle). - /// - SignedLessOrEqual, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrInstructions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrInstructions.cs deleted file mode 100644 index 52c6c4e3b6a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrInstructions.cs +++ /dev/null @@ -1,777 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Globalization; - -namespace Xamarin.Android.Tasks.LLVMIR; - -abstract class LlvmIrInstruction : LlvmIrFunctionBodyItem -{ - public string Mnemonic { get; } - public LlvmIrFunctionAttributeSet? AttributeSet { get; set; } - - /// - /// TBAA (Type Based Alias Analysis) metadata item the instruction references, if any. - /// for more information about TBAA. - /// - public LlvmIrMetadataItem? TBAA { get; set; } - - protected LlvmIrInstruction (string mnemonic) - { - if (String.IsNullOrEmpty (mnemonic)) { - throw new ArgumentException ("must not be null or empty", nameof (mnemonic)); - } - - Mnemonic = mnemonic; - } - - protected override void DoWrite (GeneratorWriteContext context, LlvmIrGenerator generator) - { - context.Output.Write (context.CurrentIndent); - WriteValueAssignment (context); - WritePreamble (context); - context.Output.Write (Mnemonic); - context.Output.Write (' '); - WriteBody (context); - - if (TBAA != null) { - context.Output.Write (", !tbaa !"); - context.Output.Write (TBAA.Name); - } - - if (AttributeSet != null) { - context.Output.Write (" #"); - context.Output.Write (AttributeSet.Number.ToString (CultureInfo.InvariantCulture)); - } - } - - /// - /// Write the '<variable_reference> = ' part of the instruction line. - /// - protected virtual void WriteValueAssignment (GeneratorWriteContext context) - {} - - /// - /// Write part of the instruction that comes between the optional value assignment and the instruction - /// mnemonic. If any text is written, it must end with a whitespace. - /// - protected virtual void WritePreamble (GeneratorWriteContext context) - {} - - /// - /// Write the "body" of the instruction, that is the part that follows instruction mnemonic but precedes the - /// metadata and attribute set references. - /// - protected virtual void WriteBody (GeneratorWriteContext context) - {} - - protected void WriteValue (GeneratorWriteContext context, Type type, object? value, bool isPointer) - { - if (value == null) { - if (!isPointer) { - throw new InvalidOperationException ($"Internal error: non-pointer type '{type}' must not have a `null` value"); - } - context.Output.Write ("null"); - } else if (value is LlvmIrVariable variable) { - context.Output.Write (variable.Reference); - } else if (value is bool) { - context.Output.Write ((bool)value ? "true" : "false"); - } else { - context.Output.Write (MonoAndroidHelper.CultureInvariantToString (value)); - } - } - - protected void WriteAlignment (GeneratorWriteContext context, ulong typeSize, bool isPointer) - { - context.Output.Write (", align "); - - ulong alignment; - if (isPointer) { - alignment = context.Target.NativePointerSize; - } else { - alignment = typeSize; - } - context.Output.Write (alignment.ToString (CultureInfo.InvariantCulture)); - } -} - -abstract class LlvmIrInstructionArgumentValuePlaceholder -{ - protected LlvmIrInstructionArgumentValuePlaceholder () - {} - - public abstract object? GetValue (LlvmIrModuleTarget target); -} - -class LlvmIrInstructionPointerSizeArgumentPlaceholder : LlvmIrInstructionArgumentValuePlaceholder -{ - public override object? GetValue (LlvmIrModuleTarget target) - { - return target.NativePointerSize; - } -} - -sealed class LlvmIrInstructions -{ - public class Alloca : LlvmIrInstruction - { - LlvmIrVariable result; - - public Alloca (LlvmIrVariable result) - : base ("alloca") - { - this.result = result; - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - if (result == null) { - return; - } - - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WriteBody (GeneratorWriteContext context) - { - string irType = LlvmIrGenerator.MapToIRType (result.Type, context.TypeCache, out ulong size, out bool isPointer); - - context.Output.Write (irType); - WriteAlignment (context, size, isPointer); - } - } - - public class Br : LlvmIrInstruction - { - const string OpName = "br"; - - LlvmIrVariable? cond; - LlvmIrFunctionLabelItem ifTrue; - LlvmIrFunctionLabelItem? ifFalse; - - /// - /// Outputs a conditional branch to label if condition is - /// true, and to label otherwise. must be a variable - /// of type bool - /// - public Br (LlvmIrVariable cond, LlvmIrFunctionLabelItem ifTrue, LlvmIrFunctionLabelItem ifFalse) - : base (OpName) - { - if (cond.Type != typeof(bool)) { - throw new ArgumentException ($"Internal error: condition must refer to a variable of type 'bool', was '{cond.Type}' instead", nameof (cond)); - } - - this.cond = cond; - this.ifTrue = ifTrue; - this.ifFalse = ifFalse; - } - - /// - /// Outputs an unconditional branch to label - /// - public Br (LlvmIrFunctionLabelItem label) - : base (OpName) - { - ifTrue = label; - } - - protected override void WriteBody (GeneratorWriteContext context) - { - if (cond == null) { - context.Output.Write ("label %"); - context.Output.Write (ifTrue.Name); - return; - } - - context.Output.Write ("i1 "); - context.Output.Write (cond.Reference); - context.Output.Write (", label %"); - context.Output.Write (ifTrue.Name); - context.Output.Write (", label %"); - context.Output.Write (ifFalse.Name); - } - } - - public class Call : LlvmIrInstruction - { - LlvmIrFunction function; - IList? arguments; - LlvmIrVariable? result; - - public LlvmIrCallMarker CallMarker { get; set; } = LlvmIrCallMarker.None; - - /// - /// This needs to be set if we want to call a function via a local or global variable. passed - /// to the constructor is then used only to generate a type safe call, while function address comes from the variable assigned - /// to this property. - /// - public LlvmIrVariable? FuncPointer { get; set; } - - public Call (LlvmIrFunction function, LlvmIrVariable? result = null, ICollection? arguments = null, LlvmIrVariable? funcPointer = null) - : base ("call") - { - this.function = function; - this.result = result; - this.FuncPointer = funcPointer; - - if (function.Signature.ReturnType != typeof(void)) { - if (result == null) { - throw new ArgumentNullException (nameof (result), $"Internal error: function '{function.Signature.Name}' returns '{function.Signature.ReturnType}' and thus requires a result variable"); - } - } else if (result != null) { - throw new ArgumentException ($"Internal error: function '{function.Signature.Name}' returns no value and yet a result variable was provided", nameof (result)); - } - - int argCount = function.Signature.Parameters.Count; - if (argCount != 0) { - if (arguments == null) { - throw new ArgumentNullException (nameof (arguments), $"Internal error: function '{function.Signature.Name}' requires {argCount} arguments"); - } - - if (function.UsesVarArgs) { - if (arguments.Count < argCount) { - throw new ArgumentException ($"Internal error: varargs function '{function.Signature.Name}' needs at least {argCount} fixed arguments, got {arguments.Count} instead"); - } - } else if (arguments.Count != argCount) { - throw new ArgumentException ($"Internal error: function '{function.Signature.Name}' requires {argCount} arguments, but {arguments.Count} were provided", nameof (arguments)); - } - - this.arguments = new List (arguments).AsReadOnly (); - } - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - if (result == null) { - return; - } - - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WritePreamble (GeneratorWriteContext context) - { - string? callMarker = CallMarker switch { - LlvmIrCallMarker.None => null, - LlvmIrCallMarker.Tail => "tail", - LlvmIrCallMarker.NoTail => "notail", - LlvmIrCallMarker.MustTail => "musttail", - _ => throw new InvalidOperationException ($"Internal error: call marker '{CallMarker}' not supported"), - }; - - if (!String.IsNullOrEmpty (callMarker)) { - context.Output.Write (callMarker); - context.Output.Write (' '); - } - } - - protected override void WriteBody (GeneratorWriteContext context) - { - if (function.ReturnsValue) { - LlvmIrGenerator.WriteReturnAttributes (context, function.Signature.ReturnAttributes); - } - - context.Output.Write (LlvmIrGenerator.MapToIRType (function.Signature.ReturnType, context.TypeCache)); - - if (function.UsesVarArgs) { - context.Output.Write (" ("); - for (int j = 0; j < function.Signature.Parameters.Count; j++) { - if (j > 0) { - context.Output.Write (", "); - } - - LlvmIrFunctionParameter parameter = function.Signature.Parameters[j]; - string irType = parameter.IsVarArgs ? "..." : LlvmIrGenerator.MapToIRType (parameter.Type, context.TypeCache); - context.Output.Write (irType); - } - context.Output.Write (')'); - } - - if (FuncPointer == null) { - context.Output.Write (" @"); - context.Output.Write (function.Signature.Name); - } else { - context.Output.Write (' '); - context.Output.Write (FuncPointer.Reference); - } - context.Output.Write ('('); - - bool isVararg = false; - int i; - for (i = 0; i < function.Signature.Parameters.Count; i++) { - if (i > 0) { - context.Output.Write (", "); - } - - LlvmIrFunctionParameter parameter = function.Signature.Parameters[i]; - if (parameter.IsVarArgs) { - isVararg = true; - } - - WriteArgument (context, parameter, i, isVararg); - } - - if (arguments != null) { - for (; i < arguments.Count; i++) { - context.Output.Write (", "); - WriteArgument (context, null, i, isVararg: true); - } - } - - context.Output.Write (')'); - } - - void WriteArgument (GeneratorWriteContext context, LlvmIrFunctionParameter? parameter, int index, bool isVararg) - { - object? value = arguments[index]; - if (value is LlvmIrInstructionArgumentValuePlaceholder placeholder) { - value = placeholder.GetValue (context.Target); - } - - string irType; - if (!isVararg) { - irType = LlvmIrGenerator.MapToIRType (parameter.Type, context.TypeCache); - } else if (value is LlvmIrVariable v1) { - irType = LlvmIrGenerator.MapToIRType (v1.Type, context.TypeCache); - } else { - if (value == null) { - // We have no way of verifying the vararg parameter type if value is null, so we'll assume it's a pointer. - // If our assumption is wrong, llc will fail and signal the error - irType = "ptr"; - } else { - irType = LlvmIrGenerator.MapToIRType (value.GetType (), context.TypeCache); - } - } - - context.Output.Write (irType); - if (parameter != null) { - LlvmIrGenerator.WriteParameterAttributes (context, parameter); - } - context.Output.Write (' '); - - if (value == null) { - if (!parameter.Type.IsNativePointer (context.TypeCache)) { - throw new InvalidOperationException ($"Internal error: value for argument {index} to function '{function.Signature.Name}' must not be null"); - } - - context.Output.Write ("null"); - return; - } - - if (value is LlvmIrVariable v2) { - context.Output.Write (v2.Reference); - return; - } - - if (parameter != null && !parameter.Type.IsAssignableFrom (value.GetType ())) { - throw new InvalidOperationException ($"Internal error: value type '{value.GetType ()}' for argument {index} to function '{function.Signature.Name}' is invalid. Expected '{parameter.Type}' or compatible"); - } - - if (value is string || value is StringHolder) { - context.Output.Write (context.Module.LookupRequiredVariableForString (StringHolder.AsHolder (value)).Reference); - return; - } - - if (LlvmIrGenerator.IsFirstClassNonPointerType (value.GetType ())) { - context.Output.Write (MonoAndroidHelper.CultureInvariantToString (value)); - return; - } - - throw new InvalidOperationException ($"Internal error: unsupported type '{value.GetType ()}' in call to function '{function.Signature.Name}'"); - } - } - - public class Ext : LlvmIrInstruction - { - const string FpextOpCode = "fpext"; - const string SextOpCode = "sext"; - const string ZextOpCode = "zext"; - - LlvmIrVariable result; - LlvmIrVariable source; - Type targetType; - - public Ext (LlvmIrVariable source, Type targetType, LlvmIrVariable result) - : base (GetOpCode (targetType)) - { - this.source = source; - this.targetType = targetType; - this.result = result; - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WriteBody (GeneratorWriteContext context) - { - context.Output.Write (LlvmIrGenerator.MapToIRType (source.Type, context.TypeCache)); - context.Output.Write (' '); - context.Output.Write (source.Reference); - context.Output.Write (" to "); - context.Output.Write ( LlvmIrGenerator.MapToIRType (targetType, context.TypeCache)); - } - - static string GetOpCode (Type targetType) - { - if (targetType == typeof(double)) { - return FpextOpCode; - } else if (targetType == typeof(int)) { - return SextOpCode; - } else if (targetType == typeof(uint)) { - return ZextOpCode; - } else { - throw new InvalidOperationException ($"Unsupported target type for upcasting: {targetType}"); - } - } - } - - public class Icmp : LlvmIrInstruction - { - LlvmIrIcmpCond cond; - LlvmIrVariable op1; - object? op2; - LlvmIrVariable result; - - public Icmp (LlvmIrIcmpCond cond, LlvmIrVariable op1, object? op2, LlvmIrVariable result) - : base ("icmp") - { - if (result.Type != typeof(bool)) { - throw new ArgumentException ($"Internal error: result must be a variable of type 'bool', was '{result.Type}' instead", nameof (result)); - } - - this.cond = cond; - this.op1 = op1; - this.op2 = op2; - this.result = result; - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WriteBody (GeneratorWriteContext context) - { - string irType = LlvmIrGenerator.MapToIRType (op1.Type, context.TypeCache, out ulong size, out bool isPointer); - string condOp = cond switch { - LlvmIrIcmpCond.Equal => "eq", - LlvmIrIcmpCond.NotEqual => "ne", - LlvmIrIcmpCond.UnsignedGreaterThan => "ugt", - LlvmIrIcmpCond.UnsignedGreaterOrEqual => "uge", - LlvmIrIcmpCond.UnsignedLessThan => "ult", - LlvmIrIcmpCond.UnsignedLessOrEqual => "ule", - LlvmIrIcmpCond.SignedGreaterThan => "sgt", - LlvmIrIcmpCond.SignedGreaterOrEqual => "sge", - LlvmIrIcmpCond.SignedLessThan => "slt", - LlvmIrIcmpCond.SignedLessOrEqual => "sle", - _ => throw new InvalidOperationException ($"Unsupported `icmp` conditional '{cond}'"), - }; - - context.Output.Write (condOp); - context.Output.Write (' '); - context.Output.Write (irType); - context.Output.Write (' '); - context.Output.Write (op1.Reference); - context.Output.Write (", "); - WriteValue (context, op1.Type, op2, isPointer); - } - } - - public class Load : LlvmIrInstruction - { - LlvmIrVariable source; - LlvmIrVariable result; - - public Load (LlvmIrVariable source, LlvmIrVariable result) - : base ("load") - { - this.source = source; - this.result = result; - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WriteBody (GeneratorWriteContext context) - { - string irType = LlvmIrGenerator.MapToIRType (result.Type, context.TypeCache, out ulong size, out bool isPointer); - context.Output.Write (irType); - context.Output.Write (", ptr "); - WriteValue (context, result.Type, source, isPointer); - WriteAlignment (context, size, isPointer); - } - } - - public class Phi : LlvmIrInstruction - { - sealed class Node - { - public readonly LlvmIrVariableReference? Variable; - public readonly LlvmIrFunctionLabelItem? Label; - - public Node (LlvmIrVariableReference? variable, LlvmIrFunctionLabelItem? label) - { - Variable = variable; - Label = label; - } - } - - LlvmIrVariable result; - - readonly List nodes; - - public Phi (LlvmIrVariable result) - : base ("phi") - { - nodes = new (); - this.result = result; - } - - /// - /// Represents the `phi` instruction form we use the most throughout marshal methods generator - one which refers to an if/else block and where - /// **both** value:label pairs are **required**. Parameters and are nullable because, in theory, - /// it is possible that hasn't had the required blocks defined prior to adding the `phi` instruction and, thus, - /// we must check for the possibility here. - /// - public Phi (LlvmIrVariable result, LlvmIrVariableReference val1, LlvmIrFunctionLabelItem? label1, LlvmIrVariableReference val2, LlvmIrFunctionLabelItem? label2) - : base ("phi") - { - this.result = result; - - nodes = new () { - new Node (val1, label1 ?? throw new ArgumentNullException (nameof (label1))), - new Node (val2, label2 ?? throw new ArgumentNullException (nameof (label2))), - }; - } - - public void AddNode (LlvmIrFunctionLabelItem label, LlvmIrVariableReference? variable) - { - nodes.Add (new Node (variable, label)); - } - - protected override void WriteValueAssignment (GeneratorWriteContext context) - { - context.Output.Write (result.Reference); - context.Output.Write (" = "); - } - - protected override void WriteBody (GeneratorWriteContext context) - { - context.Output.Write (LlvmIrGenerator.MapToIRType (result.Type, context.TypeCache)); - context.IncreaseIndent (); - bool first = true; - foreach (Node node in nodes) { - if (!first) { - context.Output.WriteLine (','); - } else { - first = false; - context.Output.WriteLine (); - } - context.Output.Write (context.CurrentIndent); - WriteNode (context, node); - } - context.DecreaseIndent (); - // context.Output.Write (" ["); - // context.Output.Write (val1.Reference); - // context.Output.Write (", %"); - // context.Output.Write (label1.Name); - // context.Output.Write ("], ["); - // context.Output.Write (val2.Reference); - // context.Output.Write (", %"); - // context.Output.Write (label2.Name); - // context.Output.Write (']'); - } - void WriteNode (GeneratorWriteContext context, Node node) - { - if (node.Label == null) { - throw new NotImplementedException ("Internal error: null labels not implemented"); - } - context.Output.Write (" ["); - context.Output.Write (node.Variable == null ? "null" : node.Variable.Reference); - context.Output.Write (", %"); - context.Output.Write (node.Label.Name); - context.Output.Write (']'); - } - } - - public class Ret : LlvmIrInstruction - { - Type retvalType; - object? retVal; - - public Ret (Type retvalType, object? retval = null) - : base ("ret") - { - this.retvalType = retvalType; - retVal = retval; - } - - protected override void WriteBody (GeneratorWriteContext context) - { - if (retvalType == typeof(void)) { - context.Output.Write ("void"); - return; - } - - string irType = LlvmIrGenerator.MapToIRType (retvalType, context.TypeCache, out bool isPointer); - context.Output.Write (irType); - context.Output.Write (' '); - - WriteValue (context, retvalType, retVal, isPointer); - } - } - - public class Store : LlvmIrInstruction - { - const string Opcode = "store"; - - object? from; - LlvmIrVariable to; - - public Store (LlvmIrVariable from, LlvmIrVariable to) - : base (Opcode) - { - this.from = from; - this.to = to; - } - - public Store (object from, LlvmIrVariable to) - : base (Opcode) - { - this.from = from; - this.to = to; - } - - /// - /// Stores `null` in the indicated variable - /// - public Store (LlvmIrVariable to) - : base (Opcode) - { - this.to = to; - } - - protected override void WriteBody (GeneratorWriteContext context) - { - Type type = from?.GetType () ?? to.Type; - string irType = LlvmIrGenerator.MapToIRType (type, context.TypeCache, out ulong size, out bool isPointer); - - context.Output.Write (irType); - context.Output.Write (' '); - - WriteValue (context, to.Type, from, isPointer); - - context.Output.Write (", ptr "); - context.Output.Write (to.Reference); - - WriteAlignment (context, size, isPointer); - } - } - - public class Switch : LlvmIrInstruction where T: struct - { - // Since we can't use System.Numerics.IBinaryInteger, this is the poor man's verification that T is acceptable for us - static readonly HashSet acceptedTypes = new () { - typeof (byte), - typeof (sbyte), - typeof (short), - typeof (ushort), - typeof (int), - typeof (uint), - typeof (long), - typeof (ulong), - }; - readonly LlvmIrVariable value; - readonly LlvmIrFunctionLabelItem defaultDest; - readonly string? automaticLabelPrefix; - ulong automaticLabelCounter = 0; - List<(T constant, LlvmIrFunctionLabelItem label, string? comment)>? items; - public Switch (LlvmIrVariable value, LlvmIrFunctionLabelItem defaultDest, string? automaticLabelPrefix = null) - : base ("switch") - { - if (!acceptedTypes.Contains (typeof(T))) { - throw new NotSupportedException ($"Type '{typeof(T)}' is unsupported, only integer types are accepted"); - } - if (value.Type != typeof (T)) { - throw new ArgumentException ($"Must refer to value of type '{typeof(T)}'", nameof (value)); - } - this.value = value; - this.defaultDest = defaultDest; - this.automaticLabelPrefix = automaticLabelPrefix; - if (!String.IsNullOrEmpty (automaticLabelPrefix)) { - items = new (); - } - } - - protected override void WriteBody (GeneratorWriteContext context) - { - string irType = LlvmIrGenerator.MapToIRType (value.Type, context.TypeCache, out _, out bool isPointer); - context.Output.Write (irType); - context.Output.Write (' '); - WriteValue (context, value.Type, value, isPointer); - context.Output.Write (", label %"); - context.Output.Write (defaultDest.Name); - context.Output.WriteLine (" ["); - context.IncreaseIndent (); - foreach ((T constant, LlvmIrFunctionLabelItem label, string? comment) in items) { - context.Output.Write (context.CurrentIndent); - context.Output.Write (irType); - context.Output.Write (' '); - context.Generator.WriteValue (context, value.Type, constant); - context.Output.Write (", label %"); - context.Output.Write (label.Name); - if (context.EmitComments && !String.IsNullOrEmpty (comment)) { - context.Output.Write (' '); - context.Generator.WriteCommentLine (context, comment); - } else { - context.Output.WriteLine (); - } - } - context.DecreaseIndent (); - context.Output.Write (context.CurrentIndent); - context.Output.Write (']'); - } - public LlvmIrFunctionLabelItem Add (T val, LlvmIrFunctionLabelItem? dest = null, string? comment = null) - { - var label = MakeLabel (dest); - items.Add ((val, label, comment)); - return label; - } - void EnsureValidity (LlvmIrFunctionLabelItem? dest) - { - if (dest != null) { - return; - } - if (String.IsNullOrEmpty (automaticLabelPrefix)) { - throw new InvalidOperationException ($"Internal error: automatic label management requested, but prefix not defined"); - } - } - LlvmIrFunctionLabelItem MakeLabel (LlvmIrFunctionLabelItem? maybeDest) - { - EnsureValidity (maybeDest); - if (maybeDest != null) { - return maybeDest; - } - var ret = new LlvmIrFunctionLabelItem (automaticLabelCounter == 0 ? automaticLabelPrefix : $"{automaticLabelPrefix}{automaticLabelCounter}"); - automaticLabelCounter++; - return ret; - } - } - - public class Unreachable : LlvmIrInstruction - { - public Unreachable () - : base ("unreachable") - {} - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrKnownMetadata.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrKnownMetadata.cs deleted file mode 100644 index ca941708b7a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrKnownMetadata.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable enable -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Contains constants for well-known LLVM IR metadata names. -/// -sealed class LlvmIrKnownMetadata -{ - /// - /// The "llvm.module.flags" metadata name used for module-level flags. - /// - public const string LlvmModuleFlags = "llvm.module.flags"; - /// - /// The "llvm.ident" metadata name used for module identification. - /// - public const string LlvmIdent = "llvm.ident"; -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrLinkage.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrLinkage.cs deleted file mode 100644 index 9e84138ae9b..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrLinkage.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Variable linkage, see https://llvm.org/docs/LangRef.html#linkage - /// - [Flags] - enum LlvmIrLinkage - { - /// - /// Default linkage (external). - /// - Default = 0 << 0, - /// - /// Private linkage - symbol is not accessible from outside the module. - /// - Private = 1 << 0, - /// - /// Internal linkage - symbol is accessible within the current module only. - /// - Internal = 1 << 1, - /// - /// Available externally linkage - symbol is available for inspection but not emission. - /// - AvailableExternally = 1 << 2, - /// - /// Link once linkage - symbol may be defined in multiple modules but only one will be chosen. - /// - LinkOnce = 1 << 3, - /// - /// Weak linkage - symbol may be overridden by other definitions. - /// - Weak = 1 << 4, - /// - /// Common linkage - for uninitialized global variables. - /// - Common = 1 << 5, - /// - /// Appending linkage - for arrays that should be concatenated with arrays from other modules. - /// - Appending = 1 << 6, - /// - /// Extern weak linkage - weak reference to external symbol. - /// - ExternWeak = 1 << 7, - /// - /// Link once ODR linkage - link once with One Definition Rule enforcement. - /// - LinkOnceODR = 1 << 8, - /// - /// External linkage - symbol is accessible from outside the module. - /// - External = 1 << 9, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrMetadataManager.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrMetadataManager.cs deleted file mode 100644 index 688b0bc0d61..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrMetadataManager.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Text; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - class LlvmIrMetadataField - { - readonly LlvmIrTypeCache cache; - - public string Contents { get; } - public bool IsReference { get; } - - public LlvmIrMetadataField (LlvmIrMetadataField other) - { - this.cache = other.cache; - Contents = other.Contents; - IsReference = other.IsReference; - } - - public LlvmIrMetadataField (LlvmIrTypeCache cache, string value, bool isReference = false) - { - this.cache = cache; - if (isReference) { - Contents = $"!{value}"; - } else { - Contents = QuoteString (value); - } - - IsReference = isReference; - } - - public LlvmIrMetadataField (LlvmIrTypeCache cache, object value) - { - this.cache = cache; - Contents = FormatValue (value); - IsReference = false; - } - - string FormatValue (object value) - { - Type vt = value.GetType (); - - if (vt == typeof(string)) { - return QuoteString ((string)value); - } - - string irType = LlvmIrGenerator.MapToIRType (vt, cache); - return $"{irType} {MonoAndroidHelper.CultureInvariantToString (value)}"; - } - - string QuoteString (string value) - { - return $"!{LlvmIrGenerator.QuoteStringNoEscape (value)}"; - } - } - - class LlvmIrMetadataItem - { - readonly LlvmIrTypeCache cache; - - List fields; - - public string Name { get; } - - public LlvmIrMetadataItem (LlvmIrMetadataItem other) - { - this.cache = other.cache; - Name = other.Name; - fields = new List (); - foreach (LlvmIrMetadataField field in other.fields) { - fields.Add (new LlvmIrMetadataField (field)); - } - } - - public LlvmIrMetadataItem (LlvmIrTypeCache cache, string name) - { - if (name.Length == 0) { - throw new ArgumentException ("must not be empty", nameof (name)); - } - - this.cache = cache; - Name = name; - fields = new List (); - } - - public void AddReferenceField (string referenceName) - { - fields.Add (new LlvmIrMetadataField (cache, referenceName, isReference: true)); - } - - public void AddReferenceField (LlvmIrMetadataItem referencedItem) - { - AddReferenceField (referencedItem.Name); - } - - public void AddField (object value) - { - AddField (new LlvmIrMetadataField (cache, value)); - } - - public void AddField (LlvmIrMetadataField field) - { - fields.Add (field); - } - - public string Render () - { - var sb = new StringBuilder ($"!{Name} = !{{"); - bool first = true; - - foreach (LlvmIrMetadataField field in fields) { - if (first) { - first = false; - } else { - sb.Append (", "); - } - - sb.Append (field.Contents); - } - - sb.Append ('}'); - - return sb.ToString (); - } - } - - class LlvmIrMetadataManager - { - readonly LlvmIrTypeCache cache; - ulong counter = 0; - List items = new List (); - Dictionary nameToItem = new Dictionary (StringComparer.Ordinal); - - public List Items => items; - - public LlvmIrMetadataManager (LlvmIrTypeCache cache) - { - this.cache = cache; - } - - public LlvmIrMetadataManager (LlvmIrMetadataManager other) - { - this.cache = other.cache; - foreach (LlvmIrMetadataItem item in other.items) { - var newItem = new LlvmIrMetadataItem (item); - items.Add (newItem); - nameToItem.Add (newItem.Name, newItem); - } - counter = other.counter; - } - - public LlvmIrMetadataItem Add (string name, params object[]? values) - { - if (nameToItem.ContainsKey (name)) { - throw new InvalidOperationException ($"Internal error: metadata item '{name}' has already been added"); - } - - var ret = new LlvmIrMetadataItem (cache, name); - - if (values != null && values.Length > 0) { - foreach (object v in values) { - ret.AddField (v); - } - } - items.Add (ret); - - nameToItem.Add (name, ret); - return ret; - } - - public LlvmIrMetadataItem AddNumbered (params object[]? values) - { - string name = counter.ToString (CultureInfo.InvariantCulture); - counter++; - return Add (name, values); - } - - public LlvmIrMetadataItem? GetItem (string name) - { - if (nameToItem.TryGetValue (name, out LlvmIrMetadataItem? item)) { - return item; - } - - return null; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModule.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModule.cs deleted file mode 100644 index 95abb042ea3..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModule.cs +++ /dev/null @@ -1,712 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -using Microsoft.Build.Utilities; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - partial class LlvmIrModule - { - /// - /// Global variable type to be used to output name:value string arrays. This is a notational shortcut, - /// do **NOT** change the type without understanding how it affects the rest of code. - /// - public static readonly Type NameValueArrayType = typeof(IDictionary); - - public IList? ExternalFunctions { get; private set; } - public IList? Functions { get; private set; } - public IList? AttributeSets { get; private set; } - public IList? Structures { get; private set; } - public IList? GlobalVariables { get; private set; } - public IList? Strings { get; private set; } - - /// - /// TBAA stands for "Type Based Alias Analysis" and is used by LLVM to implemente a description of - /// a higher level language typesystem to LLVM IR (in which memory doesn't have types). This metadata - /// item describes pointer usage for certain instructions we output and is common enough to warrant - /// a shortcut property like that. More information about TBAA can be found at https://llvm.org/docs/LangRef.html#tbaa-metadata - /// - public LlvmIrMetadataItem TbaaAnyPointer => tbaaAnyPointer; - - Dictionary? attributeSets; - Dictionary? externalFunctions; - Dictionary? functions; - Dictionary? structures; - LlvmIrStringManager? stringManager; - LlvmIrMetadataManager metadataManager; - LlvmIrMetadataItem tbaaAnyPointer; - LlvmIrBufferManager? bufferManager; - - List? globalVariables; - - LlvmIrFunction? puts; - LlvmIrFunction? abort; - - TaskLoggingHelper log; - - public readonly LlvmIrTypeCache TypeCache; - - public string? DefaultStringGroup { get; set; } - - public LlvmIrModule (LlvmIrTypeCache cache, TaskLoggingHelper log) - { - this.log = log; - TypeCache = cache; - metadataManager = new LlvmIrMetadataManager (cache); - - // Only model agnostic items can be added here - LlvmIrMetadataItem flags = metadataManager.Add (LlvmIrKnownMetadata.LlvmModuleFlags); - flags.AddReferenceField (metadataManager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "wchar_size", 4)); - flags.AddReferenceField (metadataManager.AddNumbered (LlvmIrModuleMergeBehavior.Max, "PIC Level", 2)); - - LlvmIrMetadataItem ident = metadataManager.Add (LlvmIrKnownMetadata.LlvmIdent); - LlvmIrMetadataItem identValue = metadataManager.AddNumbered ($".NET for Android {XABuildConfig.XamarinAndroidBranch} @ {XABuildConfig.XamarinAndroidCommitHash}"); - ident.AddReferenceField (identValue.Name); - - tbaaAnyPointer = metadataManager.AddNumbered (); - LlvmIrMetadataItem anyPointer = metadataManager.AddNumbered ("any pointer"); - LlvmIrMetadataItem omnipotentChar = metadataManager.AddNumbered ("omnipotent char"); - LlvmIrMetadataItem simpleCppTBAA = metadataManager.AddNumbered ("Simple C++ TBAA"); - - anyPointer.AddReferenceField (omnipotentChar.Name); - anyPointer.AddField ((ulong)0); - - omnipotentChar.AddReferenceField (simpleCppTBAA); - omnipotentChar.AddField ((ulong)0); - - tbaaAnyPointer.AddReferenceField (anyPointer); - tbaaAnyPointer.AddReferenceField (anyPointer); - tbaaAnyPointer.AddField ((ulong)0); - } - - /// - /// Return a metadata manager instance which includes copies of all the target-agnostic metadata items. - /// We must not modify the original manager since each target may have conflicting values for certain - /// flags. - /// - public LlvmIrMetadataManager GetMetadataManagerCopy () => new LlvmIrMetadataManager (metadataManager); - - /// - /// Perform any tasks that need to be done after construction is complete. - /// - public void AfterConstruction () - { - if (externalFunctions != null) { - List list = externalFunctions.Values.ToList (); - list.Sort ((LlvmIrFunction a, LlvmIrFunction b) => a.Signature.Name.CompareTo (b.Signature.Name)); - ExternalFunctions = list.AsReadOnly (); - } - - if (functions != null) { - List list = functions.Values.ToList (); - // TODO: sort or not? - Functions = list.AsReadOnly (); - } - - if (attributeSets != null) { - List list = attributeSets.Values.ToList (); - list.Sort ((LlvmIrFunctionAttributeSet a, LlvmIrFunctionAttributeSet b) => a.Number.CompareTo (b.Number)); - AttributeSets = list.AsReadOnly (); - } - - if (structures != null) { - List list = structures.Values.ToList (); - list.Sort ((StructureInfo a, StructureInfo b) => a.Name.CompareTo (b.Name)); - Structures = list.AsReadOnly (); - } - - if (stringManager != null && stringManager.StringGroups.Count > 0) { - Strings = stringManager.StringGroups.AsReadOnly (); - } - - GlobalVariables = globalVariables?.AsReadOnly (); - } - - public void Add (LlvmIrFunction func) - { - if (functions == null) { - functions = new Dictionary (); - } - - if (functions.TryGetValue (func, out LlvmIrFunction existingFunc)) { - throw new InvalidOperationException ($"Internal error: identical function has already been added (\"{func.Signature.Name}\")"); - } - - functions.Add (func, func); - } - - public LlvmIrInstructions.Call CreatePuts (string text, LlvmIrVariable result) - { - EnsurePuts (); - RegisterString (text); - return new LlvmIrInstructions.Call (puts, result, new List { text }); - } - - /// - /// Generate code to call the `puts(3)` C library function to print a simple string to standard output. - /// - public LlvmIrInstructions.Call AddPuts (LlvmIrFunction function, string text, LlvmIrVariable result) - { - EnsurePuts (); - RegisterString (text); - return function.Body.Call (puts, result, new List { text }); - } - - void EnsurePuts () - { - if (puts != null) { - return; - } - - var puts_params = new List { - new (typeof(string), "s"), - }; - - var puts_sig = new LlvmIrFunctionSignature ( - name: "puts", - returnType: typeof(int), - parameters: puts_params - ); - puts_sig.ReturnAttributes.NoUndef = true; - - puts = DeclareExternalFunction (puts_sig, MakePutsAttributeSet ()); - } - - LlvmIrFunctionAttributeSet MakePutsAttributeSet () - { - var ret = new LlvmIrFunctionAttributeSet { - new NofreeFunctionAttribute (), - new NounwindFunctionAttribute (), - }; - - ret.DoNotAddTargetSpecificAttributes = true; - return AddAttributeSet (ret); - } - - public LlvmIrInstructions.Call CreateAbort () - { - EnsureAbort (); - return new LlvmIrInstructions.Call (abort); - } - - public LlvmIrInstructions.Call AddAbort (LlvmIrFunction function) - { - EnsureAbort (); - LlvmIrInstructions.Call ret = function.Body.Call (abort); - function.Body.Unreachable (); - - return ret; - } - - void EnsureAbort () - { - if (abort != null) { - return; - } - - var abort_sig = new LlvmIrFunctionSignature (name: "abort", returnType: typeof(void)); - abort = DeclareExternalFunction (abort_sig, MakeAbortAttributeSet ()); - } - - LlvmIrFunctionAttributeSet MakeAbortAttributeSet () - { - var ret = new LlvmIrFunctionAttributeSet { - new NoreturnFunctionAttribute (), - new NounwindFunctionAttribute (), - new NoTrappingMathFunctionAttribute (true), - new StackProtectorBufferSizeFunctionAttribute (8), - }; - - return AddAttributeSet (ret); - } - - public void AddIfThenElse (LlvmIrFunction function, LlvmIrVariable result, LlvmIrIcmpCond condition, LlvmIrVariable conditionVariable, object? conditionComparand, ICollection codeIfThen, ICollection? codeIfElse = null) - { - function.Body.Icmp (condition, conditionVariable, conditionComparand, result); - - var labelIfThen = new LlvmIrFunctionLabelItem (); - LlvmIrFunctionLabelItem? labelIfElse = codeIfElse != null ? new LlvmIrFunctionLabelItem () : null; - var labelIfDone = new LlvmIrFunctionLabelItem (); - - function.Body.Br (result, labelIfThen, labelIfElse == null ? labelIfDone : labelIfElse); - function.Body.Add (labelIfThen); - - AddInstructions (codeIfThen); - - if (codeIfElse != null) { - function.Body.Add (labelIfElse); - AddInstructions (codeIfElse); - } - - function.Body.Add (labelIfDone); - - void AddInstructions (ICollection instructions) - { - foreach (LlvmIrInstruction ins in instructions) { - function.Body.Add (ins); - } - } - } - - /// - /// A shortcut way to add a global variable without first having to create an instance of first. This overload - /// requires the parameter to not be null. - /// - public LlvmIrGlobalVariable AddGlobalVariable (string name, object value, LlvmIrVariableOptions? options = null, string? comment = null) - { - if (value == null) { - throw new ArgumentNullException (nameof (value)); - } - - return AddGlobalVariable (value.GetType (), name, value, options, comment); - } - - /// - /// A shortcut way to add a global variable without first having to create an instance of first. - /// - public LlvmIrGlobalVariable AddGlobalVariable (Type type, string name, object? value, LlvmIrVariableOptions? options = null, string? comment = null) - { - LlvmIrGlobalVariable ret; - if (type == typeof(string)) { - // The cast to `string?` is intentionally meant to throw if `value` type isn't a string. - ret = new LlvmIrStringVariable (name, new StringHolder ((string?)value), options) { - Comment = comment, - }; - AddStringGlobalVariable ((LlvmIrStringVariable)ret); - } else { - ret = new LlvmIrGlobalVariable (type, name, options) { - Value = value, - Comment = comment, - }; - Add (ret); - } - - return ret; - } - - public void Add (LlvmIrGlobalVariable variable, string stringGroupName, string? stringGroupComment = null, string? symbolSuffix = null) - { - EnsureValidGlobalVariableType (variable); - - if (IsStringVariable (variable)) { - AddStringGlobalVariable ((LlvmIrStringVariable)variable, stringGroupName, stringGroupComment, symbolSuffix); - return; - } - - if (IsStringArrayVariable (variable)) { - AddStringArrayGlobalVariable (variable, stringGroupName, stringGroupComment, symbolSuffix); - return; - } - - throw new InvalidOperationException ("Internal error: this overload is ONLY for adding string or array-of-string variables"); - } - - public void Add (IList variables) - { - foreach (LlvmIrGlobalVariable variable in variables) { - Add (variable); - } - } - - public void Add (LlvmIrGlobalVariable variable) - { - EnsureValidGlobalVariableType (variable); - - if (IsStringVariable (variable)) { - AddStringGlobalVariable ((LlvmIrStringVariable)variable); - return; - } - - if (IsStringArrayVariable (variable)) { - AddStringArrayGlobalVariable (variable); - return; - } - - if (IsStructureArrayVariable (variable)) { - AddStructureArrayGlobalVariable (variable); - return; - } - - if (IsStructureVariable (variable)) { - PrepareStructure (variable); - } - - AddStandardGlobalVariable (variable); - } - - void PrepareStructure (LlvmIrGlobalVariable variable) - { - var structure = variable.Value as StructureInstance; - if (structure == null) { - return; - } - - PrepareStructure (structure); - } - - void PrepareStructure (StructureInstance structure) - { - foreach (StructureMemberInfo smi in structure.Info.Members) { - if (smi.IsIRStruct) { - object? instance = structure.Obj == null ? null : smi.GetValue (structure.Obj); - if (instance == null) { - continue; - } - - StructureInfo si = GetStructureInfo (smi.MemberType); - PrepareStructure (new GeneratorStructureInstance (si, instance)); - continue; - } - - if (smi.Info.IsNativePointerToPreallocatedBuffer (TypeCache, out ulong bufferSize)) { - if (bufferSize == 0) { - bufferSize = structure.Info.GetBufferSizeFromProvider (smi, structure); - } - - AddAutomaticBuffer (structure, smi, bufferSize); - continue; - } - - if (smi.MemberType != typeof(string)) { - continue; - } - - string? value = smi.GetValue (structure.Obj) as string; - if (value != null) { - RegisterString (value, stringGroupName: structure.Info.Name, symbolSuffix: smi.Info.Name, encoding: smi.Info.GetStringEncoding (TypeCache)); - } - } - } - - void AddAutomaticBuffer (StructureInstance structure, StructureMemberInfo smi, ulong bufferSize) - { - if (bufferManager == null) { - bufferManager = new LlvmIrBufferManager (); - } - - string bufferName = bufferManager.Allocate (structure, smi, bufferSize); - var buffer = new LlvmIrGlobalVariable (typeof(List), bufferName, LlvmIrVariableOptions.LocalWritable) { - ZeroInitializeArray = true, - ArrayItemCount = bufferSize, - }; - Add (buffer); - } - - void AddStandardGlobalVariable (LlvmIrGlobalVariable variable) - { - if (globalVariables == null) { - globalVariables = new List (); - } - - globalVariables.Add (variable); - } - - [MemberNotNull (nameof (stringManager))] - void EnsureStringManager () - { - if (stringManager == null) { - stringManager = new LlvmIrStringManager (log, DefaultStringGroup); - } - } - - void AddStringGlobalVariable (LlvmIrStringVariable variable, string? stringGroupName = null, string? stringGroupComment = null, string? symbolSuffix = null) - { - RegisterString (variable, stringGroupName, stringGroupComment, symbolSuffix); - AddStandardGlobalVariable (variable); - } - - public void RegisterString (LlvmIrStringVariable variable, string? stringGroupName = null, string? stringGroupComment = null, string? symbolSuffix = null) - { - EnsureStringManager (); - stringManager.Add (variable, stringGroupName, stringGroupComment, symbolSuffix); - } - - public void RegisterString (string value, string? stringGroupName = null, string? stringGroupComment = null, string? symbolSuffix = null, - LlvmIrStringEncoding encoding = LlvmIrStringEncoding.UTF8, StringComparison comparison = StringComparison.Ordinal) - { - EnsureStringManager (); - stringManager.Add (value, stringGroupName, stringGroupComment, symbolSuffix, encoding, comparison); - } - - void AddStructureArrayGlobalVariable (LlvmIrGlobalVariable variable) - { - if (variable.Value == null) { - AddStandardGlobalVariable (variable); - return; - } - - // For simplicity we support only arrays with homogenous entry types - StructureInfo? info = null; - ulong index = 0; - - foreach (StructureInstance structure in (IEnumerable)variable.Value) { - if (info == null) { - info = structure.Info; - if (info.HasPreAllocatedBuffers) { - // let's group them... - Add (new LlvmIrGroupDelimiterVariable ()); - } - } - - if (structure.Type != info.Type) { - throw new InvalidOperationException ($"Internal error: only arrays with homogenous element types are currently supported. All entries were expected to be of type '{info.Type}', but the '{structure.Type}' type was encountered."); - } - - // This is a bit of a kludge to make a specific corner case work seamlessly from the LlvmIrModule user's point of view. - // The scenario is used in ApplicationConfigNativeAssemblyGenerator and it involves an array of structures where each - // array index contains the same object in structure.Obj but each instance needs to allocate a unique buffer at runtime. - // LlvmIrBufferManager makes it possible, but it must be able to uniquely identify each instance, which in this scenario - // wouldn't be possible if we had to rely only on the StructureInstance contents. Enter `StructureInstance.IndexInArray`, - // which is used to create unique buffers and unambiguously assign them to each structure instance. - // - // See LlvmIrBufferManager for how it is used. - structure.IndexInArray = index++; - - PrepareStructure (structure); - } - - if (info != null && info.HasPreAllocatedBuffers) { - Add (new LlvmIrGroupDelimiterVariable ()); - } - - AddStandardGlobalVariable (variable); - } - - void AddStringArrayGlobalVariable (LlvmIrGlobalVariable variable, string? stringGroupName = null, string? stringGroupComment = null, string? symbolSuffix = null) - { - if (variable.Value == null) { - AddStandardGlobalVariable (variable); - return; - } - - List? entries = null; - if (NameValueArrayType.IsAssignableFrom (variable.Type)) { - entries = new List (); - var dict = (IDictionary)variable.Value; - foreach (var kvp in dict) { - Register (kvp.Key); - Register (kvp.Value); - } - } else if (typeof(ICollection).IsAssignableFrom (variable.Type)) { - foreach (string? s in (ICollection)variable.Value) { - Register (s); - } - } else { - throw new InvalidOperationException ($"Internal error: unsupported string array type `{variable.Type}'"); - } - - AddStandardGlobalVariable (variable); - - void Register (string? value) - { - if (value == null) { - return; - } - - RegisterString (value, stringGroupName, stringGroupComment, symbolSuffix); - } - } - - bool IsStringArrayVariable (LlvmIrGlobalVariable variable) - { - if (NameValueArrayType.IsAssignableFrom (variable.Type)) { - if (variable.Value != null && !NameValueArrayType.IsAssignableFrom (variable.Value.GetType ())) { - throw new InvalidOperationException ($"Internal error: name:value array variable must have its value set to either `null` or `{NameValueArrayType}`"); - } - - return true; - } - - var ctype = typeof(ICollection); - if (ctype.IsAssignableFrom (variable.Type)) { - if (variable.Value != null && !ctype.IsAssignableFrom (variable.Value.GetType ())) { - throw new InvalidOperationException ($"Internal error: string array variable must have its value set to either `null` or implement `{ctype}`"); - } - - return true; - } - - if (variable.Type == typeof(string[])) { - if (variable.Value != null && variable.Value.GetType () != typeof(string[])) { - throw new InvalidOperationException ($"Internal error: string array variable must have its value set to either `null` or be `{typeof(string[])}`"); - } - - return true; - } - - return false; - } - - bool IsStringVariable (LlvmIrGlobalVariable variable) - { - if (variable.Type != typeof(string)) { - return false; - } - - if (variable.Value != null && variable.Value.GetType () != typeof(string)) { - throw new InvalidOperationException ("Internal error: variable of string type must have its value set to either `null` or a string"); - } - - return true; - } - - bool IsStructureArrayVariable (LlvmIrGlobalVariable variable) - { - if (typeof(StructureInstance[]).IsAssignableFrom (variable.Type)) { - return true; - } - - if (!variable.Type.IsArray ()) { - return false; - } - - Type elementType = variable.Type.GetArrayElementType (); - return typeof(StructureInstance).IsAssignableFrom (elementType); - } - - bool IsStructureVariable (LlvmIrGlobalVariable variable) - { - if (!typeof(StructureInstance).IsAssignableFrom (variable.Type)) { - return false; - } - - if (variable.Value != null && !typeof(StructureInstance).IsAssignableFrom (variable.Value.GetType ())) { - throw new InvalidOperationException ("Internal error: variable referring to a structure instance must have its value set to either `null` or an instance of the StructureInstance class"); - } - - return true; - } - - void EnsureValidGlobalVariableType (LlvmIrGlobalVariable variable) - { - if (variable is LlvmIrStringVariable) { - throw new ArgumentException ("Internal error: do not add instances of LlvmIrStringVariable, simply set variable value to the desired string instead"); - } - } - - /// - /// Looks up LLVM variable for a previously registered string given in . If a variable isn't found, - /// an exception is thrown. This is primarily used by to look up variables related to strings which - /// are part of structure instances. Such strings **MUST** be registered by and, thus, failure to do - /// so is an internal error. - /// - public LlvmIrStringVariable LookupRequiredVariableForString (StringHolder value) - { - LlvmIrStringVariable? sv = stringManager?.Lookup (value); - if (sv == null) { - throw new InvalidOperationException ($"Internal error: string '{value}' wasn't registered with string manager"); - } - - return sv; - } - - public string LookupRequiredBufferVariableName (StructureInstance structure, StructureMemberInfo smi) - { - if (bufferManager == null) { - throw new InvalidOperationException ("Internal error: no buffer variables have been registed with the buffer manager"); - } - - string? variableName = bufferManager.GetBufferVariableName (structure, smi); - if (variableName.IsNullOrEmpty ()) { - throw new InvalidOperationException ($"Internal error: buffer for member '{smi.Info.Name}' of structure '{structure.Info.Name}' (index {structure.IndexInArray}) not found"); - } - - return variableName; - } - - /// - /// Add a new attribute set. The caller MUST use the returned value to refer to the set, instead of the one passed - /// as parameter, since this function de-duplicates sets and may return a previously added one that's identical to - /// the new one. - /// - public LlvmIrFunctionAttributeSet AddAttributeSet (LlvmIrFunctionAttributeSet attrSet) - { - if (attributeSets == null) { - attributeSets = new Dictionary (); - } - - if (attributeSets.TryGetValue (attrSet, out LlvmIrFunctionAttributeSet existingSet)) { - return existingSet; - } - attrSet.Number = (uint)attributeSets.Count; - attributeSets.Add (attrSet, attrSet); - - return attrSet; - } - - /// - /// Add a new external function declaration. The caller MUST use the returned value to refer to the function, instead - /// of the one passed as parameter, since this function de-duplicates function declarations and may return a previously - /// added one that's identical to the new one. - /// - public LlvmIrFunction DeclareExternalFunction (LlvmIrFunction func) - { - if (externalFunctions == null) { - externalFunctions = new Dictionary (); - } - - if (externalFunctions.TryGetValue (func, out LlvmIrFunction existingFunc)) { - return existingFunc; - } - - externalFunctions.Add (func, func); - return func; - } - - public LlvmIrFunction DeclareExternalFunction (LlvmIrFunctionSignature sig, LlvmIrFunctionAttributeSet? attrSet = null) - { - return DeclareExternalFunction (new LlvmIrFunction (sig, attrSet)); - } - - /// - /// Since LLVM IR is strongly typed, it requires each structure to be properly declared before it is - /// used throughout the code. This method uses reflection to scan the managed type - /// and record the information for future use. The returned structure contains - /// the description. It is used later on not only to declare the structure in output code, but also to generate - /// data from instances of . This method is typically called from the - /// method. - /// - public StructureInfo MapStructure () - { - if (structures == null) { - structures = new Dictionary (); - } - - Type t = typeof(T); - if (!t.IsClass && !t.IsValueType) { - throw new InvalidOperationException ($"{t} must be a class or a struct"); - } - - if (structures.TryGetValue (t, out StructureInfo sinfo)) { - return (StructureInfo)sinfo; - } - - var ret = new StructureInfo (this, t, TypeCache); - structures.Add (t, ret); - - return ret; - } - - internal StructureInfo GetStructureInfo (Type type) - { - if (structures == null) { - throw new InvalidOperationException ($"Internal error: no structures have been mapped, cannot return info for {type}"); - } - - foreach (var kvp in structures) { - StructureInfo si = kvp.Value; - if (si.Type != type) { - continue; - } - - return si; - } - - throw new InvalidOperationException ($"Internal error: unmapped structure {type}"); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleAArch64.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleAArch64.cs deleted file mode 100644 index fbc7d1357aa..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleAArch64.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Generic; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// LLVM IR module target implementation for the AArch64 (ARM 64-bit) architecture. -/// Provides AArch64-specific data layout, target triple, and compilation settings. -/// -class LlvmIrModuleAArch64 : LlvmIrModuleTarget -{ - /// - /// Gets the data layout specification for the AArch64 architecture. - /// - public override LlvmIrDataLayout DataLayout { get; } - /// - /// Gets the LLVM target triple for AArch64 Android. - /// - public override string Triple => "aarch64-unknown-linux-android21"; - /// - /// Gets the Android target architecture (ARM 64-bit). - /// - public override AndroidTargetArch TargetArch => AndroidTargetArch.Arm64; - /// - /// Gets the size of native pointers in bytes (8 for 64-bit architecture). - /// - public override uint NativePointerSize => 8; - /// - /// Gets a value indicating whether this is a 64-bit architecture. - /// - public override bool Is64Bit => true; - - /// - /// Initializes a new instance of the class. - /// Sets up the AArch64-specific data layout based on Android NDK specifications. - /// - public LlvmIrModuleAArch64 () - { - // - // As per Android NDK: - // target datalayout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128" - // - DataLayout = new LlvmIrDataLayout { - LittleEndian = true, - Mangling = new LlvmIrDataLayoutMangling (LlvmIrDataLayoutManglingOption.ELF), - - IntegerAlignment = new List { - new LlvmIrDataLayoutIntegerAlignment (size: 8, abi: 8, pref: 32), // i8 - new LlvmIrDataLayoutIntegerAlignment (size: 16, abi: 16, pref: 32), // i16 - new LlvmIrDataLayoutIntegerAlignment (size: 64, abi: 64), // i64 - new LlvmIrDataLayoutIntegerAlignment (size: 128, abi: 128), // i128 - }, - - NativeIntegerWidths = new List { 32, 64}, - StackAlignment = 128, - }; - } - - /// - /// Adds AArch64-specific attributes to the function attribute set. - /// - /// The function attribute set to add AArch64-specific attributes to. - public override void AddTargetSpecificAttributes (LlvmIrFunctionAttributeSet attrSet) - { - attrSet.Add (new TargetCpuFunctionAttribute ("generic")); - attrSet.Add (new TargetFeaturesFunctionAttribute ("+fix-cortex-a53-835769,+neon,+outline-atomics,+v8a")); - } - - /// - /// Adds AArch64-specific metadata to the metadata manager. - /// - /// The metadata manager to add AArch64-specific metadata to. - public override void AddTargetSpecificMetadata (LlvmIrMetadataManager manager) - { - LlvmIrMetadataItem flags = GetFlagsMetadata (manager); - - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "branch-target-enforcement", 0)); - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "sign-return-address", 0)); - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "sign-return-address-all", 0)); - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "sign-return-address-with-bkey", 0)); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleArmV7a.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleArmV7a.cs deleted file mode 100644 index f7ff54f8a48..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleArmV7a.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Collections.Generic; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -class LlvmIrModuleArmV7a : LlvmIrModuleTarget -{ - public override LlvmIrDataLayout DataLayout { get; } - public override string Triple => "armv7-unknown-linux-android21"; - public override AndroidTargetArch TargetArch => AndroidTargetArch.Arm; - public override uint NativePointerSize => 4; - public override bool Is64Bit => false; - - public LlvmIrModuleArmV7a () - { - // - // As per Android NDK: - // target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" - // - DataLayout = new LlvmIrDataLayout { - LittleEndian = true, - Mangling = new LlvmIrDataLayoutMangling (LlvmIrDataLayoutManglingOption.ELF), - - PointerSize = new List { - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32), - }, - - FunctionPointerAlignment = new LlvmIrDataLayoutFunctionPointerAlignment (LlvmIrDataLayoutFunctionPointerAlignmentType.Independent, abi: 8), - - IntegerAlignment = new List { - new LlvmIrDataLayoutIntegerAlignment (size: 64, abi: 64), // i64 - }, - - VectorAlignment = new List { - new LlvmIrDataLayoutVectorAlignment (size: 128, abi: 64, pref: 128), // v128 - }, - - AggregateObjectAlignment = new LlvmIrDataLayoutAggregateObjectAlignment (abi: 0, pref: 32), - NativeIntegerWidths = new List { 32 }, - StackAlignment = 64, - }; - } - - public override void AddTargetSpecificAttributes (LlvmIrFunctionAttributeSet attrSet) - { - attrSet.Add (new TargetCpuFunctionAttribute ("generic")); - attrSet.Add (new TargetFeaturesFunctionAttribute ("+armv7-a,+d32,+dsp,+fp64,+neon,+vfp2,+vfp2sp,+vfp3,+vfp3d16,+vfp3d16sp,+vfp3sp,-aes,-fp-armv8,-fp-armv8d16,-fp-armv8d16sp,-fp-armv8sp,-fp16,-fp16fml,-fullfp16,-sha2,-thumb-mode,-vfp4,-vfp4d16,-vfp4d16sp,-vfp4sp")); - } - - public override void SetParameterFlags (LlvmIrFunctionParameter parameter) - { - base.SetParameterFlags (parameter); - SetIntegerParameterUpcastFlags (parameter); - } - - public override void AddTargetSpecificMetadata (LlvmIrMetadataManager manager) - { - LlvmIrMetadataItem flags = GetFlagsMetadata (manager); - - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "min_enum_size", 4)); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleMergeBehavior.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleMergeBehavior.cs deleted file mode 100644 index 62254347fea..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleMergeBehavior.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Constants defining LLVM IR module merge behaviors for module flags metadata. - /// See: https://llvm.org/docs/LangRef.html#module-flags-metadata - /// - static class LlvmIrModuleMergeBehavior - { - /// - /// Error merge behavior - linking fails if flag values differ. - /// - public const int Error = 1; - /// - /// Warning merge behavior - warning is emitted if flag values differ. - /// - public const int Warning = 2; - /// - /// Require merge behavior - linking fails if the flag is not present in the other module. - /// - public const int Require = 3; - /// - /// Override merge behavior - the flag value is overridden. - /// - public const int Override = 4; - /// - /// Append merge behavior - the flag values are appended. - /// - public const int Append = 5; - /// - /// Append unique merge behavior - the flag values are appended only if unique. - /// - public const int AppendUnique = 6; - /// - /// Max merge behavior - the maximum value is used. - /// - public const int Max = 7; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleTarget.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleTarget.cs deleted file mode 100644 index 6b6226fcf27..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleTarget.cs +++ /dev/null @@ -1,113 +0,0 @@ -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Abstract base class for LLVM IR module targets that define architecture-specific code generation settings. -/// Each target architecture implements this class to provide specific data layout, triple, and compilation settings. -/// -abstract class LlvmIrModuleTarget -{ - /// - /// Gets the data layout specification for this target architecture. - /// - public abstract LlvmIrDataLayout DataLayout { get; } - /// - /// Gets the LLVM target triple for this architecture. - /// - public abstract string Triple { get; } - /// - /// Gets the Android target architecture this module target represents. - /// - public abstract AndroidTargetArch TargetArch { get; } - /// - /// Gets the size of native pointers in bytes for this target architecture. - /// - public abstract uint NativePointerSize { get; } - /// - /// Gets a value indicating whether this target architecture is 64-bit. - /// - public abstract bool Is64Bit { get; } - - /// - /// Adds target-specific attributes which are common to many attribute sets. Usually this specifies CPU type, tuning and - /// features. - /// - /// The function attribute set to add target-specific attributes to. - public virtual void AddTargetSpecificAttributes (LlvmIrFunctionAttributeSet attrSet) - {} - - /// - /// Adds target-specific metadata to the metadata manager. - /// - /// The metadata manager to add target-specific metadata to. - public virtual void AddTargetSpecificMetadata (LlvmIrMetadataManager manager) - {} - - /// - /// Sets target-specific parameter flags for function parameters. - /// - /// The function parameter to set flags on. - public virtual void SetParameterFlags (LlvmIrFunctionParameter parameter) - { - if (!parameter.NoUndef.HasValue) { - parameter.NoUndef = true; - } - } - - /// - /// Sets the zeroext or signext attributes on the parameter, if not set previously and if - /// the parameter is a small integral type. Out of our supported architectures, all except AArch64 set - /// the flags, thus the reason to put this method in the base class. - /// - /// The function parameter to potentially set upcasting flags on. - protected void SetIntegerParameterUpcastFlags (LlvmIrFunctionParameter parameter) - { - if (parameter.Type == typeof(bool) || - parameter.Type == typeof(byte) || - parameter.Type == typeof(char) || - parameter.Type == typeof(ushort)) - { - if (!parameter.ZeroExt.HasValue) { - parameter.ZeroExt = true; - parameter.SignExt = false; - } - return; - } - - if (parameter.Type == typeof(sbyte) || - parameter.Type == typeof(short)) - { - if (!parameter.SignExt.HasValue) { - parameter.SignExt = true; - parameter.ZeroExt = false; - } - } - } - - /// - /// Gets the alignment for aggregate objects (structures, arrays) based on the maximum field alignment and data size. - /// - /// The maximum alignment requirement of any field in the aggregate. - /// The total size of the aggregate data. - /// The alignment to use for the aggregate object. - public virtual int GetAggregateAlignment (int maxFieldAlignment, ulong dataSize) - { - return maxFieldAlignment; - } - - /// - /// Gets or creates the module flags metadata item for this target. - /// - /// The metadata manager to get the flags from. - /// The module flags metadata item. - protected LlvmIrMetadataItem GetFlagsMetadata (LlvmIrMetadataManager manager) - { - LlvmIrMetadataItem? flags = manager.GetItem (LlvmIrKnownMetadata.LlvmModuleFlags); - if (flags == null) { - flags = manager.Add (LlvmIrKnownMetadata.LlvmModuleFlags); - } - - return flags; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX64.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX64.cs deleted file mode 100644 index ccd4140b824..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX64.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// LLVM IR module target implementation for the x86-64 (Intel/AMD 64-bit) architecture. -/// Provides x86-64-specific data layout, target triple, and compilation settings. -/// -class LlvmIrModuleX64 : LlvmIrModuleTarget -{ - /// - /// Gets the data layout specification for the x86-64 architecture. - /// - public override LlvmIrDataLayout DataLayout { get; } - /// - /// Gets the LLVM target triple for x86-64 Android. - /// - public override string Triple => "x86_64-unknown-linux-android21"; - /// - /// Gets the Android target architecture (x86-64). - /// - public override AndroidTargetArch TargetArch => AndroidTargetArch.X86_64; - /// - /// Gets the size of native pointers in bytes (8 for 64-bit architecture). - /// - public override uint NativePointerSize => 8; - /// - /// Gets a value indicating whether this is a 64-bit architecture. - /// - public override bool Is64Bit => true; - - /// - /// Initializes a new instance of the class. - /// Sets up the x86-64-specific data layout based on Android NDK specifications. - /// - public LlvmIrModuleX64 () - { - // - // As per Android NDK: - // target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" - // - DataLayout = new LlvmIrDataLayout { - LittleEndian = true, - Mangling = new LlvmIrDataLayoutMangling (LlvmIrDataLayoutManglingOption.ELF), - - PointerSize = new List { - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32) { - AddressSpace = 270, - }, - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32) { - AddressSpace = 271, - }, - new LlvmIrDataLayoutPointerSize (size: 64, abi: 64) { - AddressSpace = 272, - }, - }, - - IntegerAlignment = new List { - new LlvmIrDataLayoutIntegerAlignment (size: 64, abi: 64), // i64 - }, - - FloatAlignment = new List { - new LlvmIrDataLayoutFloatAlignment (size: 80, abi: 128), // f80 - }, - - NativeIntegerWidths = new List { 8, 16, 32, 64 }, - StackAlignment = 128, - }; - } - - /// - /// Adds x86-64-specific attributes to the function attribute set. - /// - /// The function attribute set to add x86-64-specific attributes to. - public override void AddTargetSpecificAttributes (LlvmIrFunctionAttributeSet attrSet) - { - attrSet.Add (new TargetCpuFunctionAttribute ("x86-64")); - attrSet.Add (new TargetFeaturesFunctionAttribute ("+crc32,+cx16,+cx8,+fxsr,+mmx,+popcnt,+sse,+sse2,+sse3,+sse4.1,+sse4.2,+ssse3,+x87")); - attrSet.Add (new TuneCpuFunctionAttribute ("generic")); - } - - /// - /// Sets x86-64-specific parameter flags for function parameters. - /// - /// The function parameter to set flags on. - public override void SetParameterFlags (LlvmIrFunctionParameter parameter) - { - base.SetParameterFlags (parameter); - SetIntegerParameterUpcastFlags (parameter); - } - - /// - /// Gets the alignment for aggregate objects according to the System V ABI for x86-64. - /// Aggregates 16 bytes or larger must be aligned to at least 16 bytes. - /// - /// The maximum alignment requirement of any field in the aggregate. - /// The total size of the aggregate data. - /// The alignment to use for the aggregate object. - public override int GetAggregateAlignment (int maxFieldAlignment, ulong dataSize) - { - // System V ABI for x86_64 mandates that any aggregates 16 bytes or more long will - // be aligned at at least 16 bytes - // - // See: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf (Section '3.1.2 Data Representation', "Aggregates and Unions") - // - if (dataSize >= 16 && maxFieldAlignment < 16) { - return 16; - } - - return maxFieldAlignment; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX86.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX86.cs deleted file mode 100644 index d06a5f1a0a7..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrModuleX86.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// LLVM IR module target implementation for the x86 (Intel/AMD 32-bit) architecture. -/// Provides x86-specific data layout, target triple, and compilation settings. -/// -class LlvmIrModuleX86 : LlvmIrModuleTarget -{ - /// - /// Gets the data layout specification for the x86 architecture. - /// - public override LlvmIrDataLayout DataLayout { get; } - /// - /// Gets the LLVM target triple for x86 Android. - /// - public override string Triple => "i686-unknown-linux-android21"; - /// - /// Gets the Android target architecture (x86). - /// - public override AndroidTargetArch TargetArch => AndroidTargetArch.X86; - /// - /// Gets the size of native pointers in bytes (4 for 32-bit architecture). - /// - public override uint NativePointerSize => 4; - /// - /// Gets a value indicating whether this is a 64-bit architecture. - /// - public override bool Is64Bit => false; - - /// - /// Initializes a new instance of the class. - /// Sets up the x86-specific data layout based on Android NDK specifications. - /// - public LlvmIrModuleX86 () - { - // - // As per Android NDK: - // target datalayout = "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128" - // - DataLayout = new LlvmIrDataLayout { - LittleEndian = true, - Mangling = new LlvmIrDataLayoutMangling (LlvmIrDataLayoutManglingOption.ELF), - - PointerSize = new List { - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32), - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32) { - AddressSpace = 270, - }, - new LlvmIrDataLayoutPointerSize (size: 32, abi: 32) { - AddressSpace = 271, - }, - new LlvmIrDataLayoutPointerSize (size: 64, abi: 64) { - AddressSpace = 272, - }, - }, - - FloatAlignment = new List { - new LlvmIrDataLayoutFloatAlignment (size: 64, abi: 32, pref: 64), // f64 - new LlvmIrDataLayoutFloatAlignment (size: 80, abi: 32), // f80 - }, - - NativeIntegerWidths = new List { 8, 16, 32 }, - StackAlignment = 128, - }; - } - - /// - /// Adds x86-specific attributes to the function attribute set. - /// - /// The function attribute set to add x86-specific attributes to. - public override void AddTargetSpecificAttributes (LlvmIrFunctionAttributeSet attrSet) - { - attrSet.Add (new TargetCpuFunctionAttribute ("i686")); - attrSet.Add (new TargetFeaturesFunctionAttribute ("+cx8,+mmx,+sse,+sse2,+sse3,+ssse3,+x87")); - attrSet.Add (new TuneCpuFunctionAttribute ("generic")); - attrSet.Add (new StackrealignFunctionAttribute ()); - } - - /// - /// Sets x86-specific parameter flags for function parameters. - /// - /// The function parameter to set flags on. - public override void SetParameterFlags (LlvmIrFunctionParameter parameter) - { - base.SetParameterFlags (parameter); - SetIntegerParameterUpcastFlags (parameter); - } - - /// - /// Adds x86-specific metadata to the metadata manager. - /// - /// The metadata manager to add x86-specific metadata to. - public override void AddTargetSpecificMetadata (LlvmIrMetadataManager manager) - { - LlvmIrMetadataItem flags = GetFlagsMetadata (manager); - - flags.AddReferenceField (manager.AddNumbered (LlvmIrModuleMergeBehavior.Error, "NumRegisterParameters", 0)); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrRuntimePreemption.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrRuntimePreemption.cs deleted file mode 100644 index e44affe6e69..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrRuntimePreemption.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Runtime preemption specifiers, see https://llvm.org/docs/LangRef.html#runtime-preemption-model - /// - [Flags] - enum LlvmIrRuntimePreemption - { - /// - /// Default runtime preemption (dso_preemptable). - /// - Default = 0 << 0, - /// - /// DSO preemptable - symbol may be preempted by symbols from other modules. - /// - DSOPreemptable = 1 << 0, - /// - /// DSO local - symbol is local to the current dynamic shared object. - /// - DSOLocal = 1 << 1, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrSectionedArray.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrSectionedArray.cs deleted file mode 100644 index d868f776429..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrSectionedArray.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Represents a uniform array which is composed of several sections. Each section must contain -/// entries of the same type (or derived from the same base type), has its own header but is otherwise -/// treated as a separate entity to other sections. The resulting output array will look as a flattened -/// array of arrays (sections). -/// -abstract class LlvmIrSectionedArrayBase -{ - readonly Type containedType; - readonly List sections = new (); - - public List Sections => sections; - public Type ContainedType => containedType; - public ulong Count => GetItemCount (); - - protected LlvmIrSectionedArrayBase (Type containedType) - { - this.containedType = containedType; - } - - protected void Add (LlvmIrArraySectionBase section) - { - if (!containedType.IsAssignableFrom (section.DataType)) { - throw new ArgumentException ("must be of type {containedType} or derived from it", nameof (section)); - } - - sections.Add (section); - } - - ulong GetItemCount () - { - ulong ret = 0; - foreach (LlvmIrArraySectionBase section in sections) { - ret += (ulong)section.Data.Count; - } - return ret; - } -} - -class LlvmIrSectionedArray : LlvmIrSectionedArrayBase -{ - public LlvmIrSectionedArray () - : base (typeof (T)) - {} - - public void Add (LlvmIrArraySection section) => base.Add (section); -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringBlob.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringBlob.cs deleted file mode 100644 index fbab4fcf72c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringBlob.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// This class is an optimization which allows us to store strings -/// as a single "blob" of data where each string follows another, all -/// of them separated with a NUL character. This allows us to use a single -/// pointer at run time instead of several (one per string). The result is -/// less relocations in the final .so, which is good for performance -/// -/// Each string is converted to UTF8 before storing as a byte array. To optimize -/// for size, duplicate strings are not stored, instead the earlier offset+length -/// are returned when calling the method. -/// -/// -class LlvmIrStringBlob -{ - // Length is one more than byte size, to account for the terminating nul - public record struct StringInfo (int Offset, int Length, byte[] Bytes, string Value); - - Dictionary cache = new (StringComparer.Ordinal); - List segments = new (); - long size = 0; - - public long Size => size; - - /// - /// Add string to the blob, returning its offset in the blob - /// and its size (which is the string length after conversion from UTF8 to a byte array plus - /// 1 for the terminating NUL character) - /// - public (int offset, int length) Add (string s) - { - if (cache.TryGetValue (s, out StringInfo info)) { - return (info.Offset, info.Length); - } - - byte[] bytes = MonoAndroidHelper.Utf8StringToBytes (s); - int offset; - if (segments.Count > 0) { - StringInfo lastSegment = segments[segments.Count - 1]; - offset = lastSegment.Offset + lastSegment.Length + 1; // Include trailing NUL here - } else { - offset = 0; - } - - info = new StringInfo ( - Offset: offset, - Length: bytes.Length, - Bytes: bytes, - Value: s - ); - segments.Add (info); - cache.Add (s, info); - size += info.Length + 1; // Account for the trailing NUL - - return (info.Offset, info.Length); - } - - public int GetIndexOf (string s) - { - if (String.IsNullOrEmpty (s)) { - return -1; - } - - if (!cache.TryGetValue (s, out StringInfo info)) { - return -1; - } - - return info.Offset; - } - - public IEnumerable GetSegments () - { - foreach (StringInfo si in segments) { - yield return si; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringEncoding.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringEncoding.cs deleted file mode 100644 index 1a257a72dd9..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringEncoding.cs +++ /dev/null @@ -1,17 +0,0 @@ -#nullable enable -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Specifies the encoding used for string variables in LLVM IR. -/// -enum LlvmIrStringEncoding -{ - /// - /// UTF-8 encoding using i8 LLVM IR type. - /// - UTF8, - /// - /// Unicode (UTF-16) encoding using i16 LLVM IR type. - /// - Unicode, -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringGroup.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringGroup.cs deleted file mode 100644 index 17318c2505f..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringGroup.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Represents a group of string variables that will be emitted together in the LLVM IR output. -/// String groups allow organizing related strings with optional comments for better code organization. -/// -sealed class LlvmIrStringGroup -{ - /// - /// Gets the number of strings in this group. - /// - public ulong Count; - /// - /// Gets the optional comment associated with this string group. - /// - public readonly string? Comment; - /// - /// Gets the list of string variables in this group. - /// - public readonly List Strings = new List (); - - /// - /// Initializes a new instance of the class. - /// - /// Optional comment to associate with this string group. - public LlvmIrStringGroup (string? comment = null) - { - Comment = comment; - Count = 0; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringManager.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringManager.cs deleted file mode 100644 index 71a8794bdbc..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrStringManager.cs +++ /dev/null @@ -1,94 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; - -using Microsoft.Build.Utilities; - -namespace Xamarin.Android.Tasks.LLVMIR; - -partial class LlvmIrModule -{ - protected class LlvmIrStringManager - { - readonly string defaultGroupName = "str"; - - Dictionary stringSymbolCache = new Dictionary (); - Dictionary stringGroupCache = new Dictionary (StringComparer.Ordinal); - List stringGroups = new List (); - - LlvmIrStringGroup defaultGroup; - TaskLoggingHelper log; - - public List StringGroups => stringGroups; - - public LlvmIrStringManager (TaskLoggingHelper log, string? defaultStringGroup = null) - { - this.log = log; - if (!defaultStringGroup.IsNullOrEmpty ()) { - defaultGroupName = defaultStringGroup; - } - - defaultGroup = new LlvmIrStringGroup (); - stringGroupCache.Add (String.Empty, defaultGroup); - stringGroups.Add (defaultGroup); - } - - public LlvmIrStringVariable Add (LlvmIrStringVariable variable, string? groupName = null, string? groupComment = null, string? symbolSuffix = null) - { - // Let it throw if Value isn't a StringHolder, it must be. - return Add((StringHolder)variable.Value!, groupName, groupComment, symbolSuffix); - } - - public LlvmIrStringVariable Add (string value, string? groupName = null, string? groupComment = null, string? symbolSuffix = null, - LlvmIrStringEncoding encoding = LlvmIrStringEncoding.UTF8, StringComparison comparison = StringComparison.Ordinal) - { - if (value == null) { - throw new ArgumentNullException (nameof (value)); - } - - return Add (new StringHolder (value, encoding, comparison), groupName, groupComment, symbolSuffix); - } - - LlvmIrStringVariable Add (StringHolder holder, string? groupName = null, string? groupComment = null, string? symbolSuffix = null) - { - if (stringSymbolCache.TryGetValue (holder, out LlvmIrStringVariable? stringVar) && stringVar != null) { - return stringVar; - } - - LlvmIrStringGroup? group; - string groupPrefix; - if (groupName.IsNullOrEmpty () || MonoAndroidHelper.StringEquals ("str", groupName)) { - group = defaultGroup; - groupPrefix = $".{defaultGroupName}"; - } else if (!stringGroupCache.TryGetValue (groupName, out group) || group == null) { - group = new LlvmIrStringGroup (groupComment ?? groupName); - stringGroups.Add (group); - stringGroupCache[groupName] = group; - groupPrefix = $".{groupName}"; - } else { - groupPrefix = $".{groupName}"; - } - - string symbolName = $"{groupPrefix}.{group.Count++}"; - if (!symbolSuffix.IsNullOrEmpty ()) { - symbolName = $"{symbolName}_{symbolSuffix}"; - } - - stringVar = new LlvmIrStringVariable (symbolName, holder); - group.Strings.Add (stringVar); - stringSymbolCache.Add (holder, stringVar); - - return stringVar; - } - - public LlvmIrStringVariable? Lookup (StringHolder value) - { - if (stringSymbolCache.TryGetValue (value, out LlvmIrStringVariable? sv)) { - return sv; - } - - return null; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrTypeCache.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrTypeCache.cs deleted file mode 100644 index 500ab392203..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrTypeCache.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Provides caching for frequently accessed type and member attributes in the LLVM IR generation process. -/// This cache improves performance by avoiding repeated reflection calls for the same attributes. -/// -class LlvmIrTypeCache -{ - readonly Dictionary pointerAttributes = []; - readonly Dictionary assemblerAttributes = []; - - /// - /// Gets the for the specified member, using caching to improve performance. - /// - /// The member info to get the attribute for. - /// The if present; otherwise, null. - public NativePointerAttribute? GetNativePointerAttribute (MemberInfo mi) - { - if (!pointerAttributes.TryGetValue (mi, out var attr)) { - pointerAttributes[mi] = attr = mi.GetCustomAttribute (); - } - return attr; - } - - /// - /// Gets the for the specified member, using caching to improve performance. - /// - /// The member info to get the attribute for. - /// The if present; otherwise, null. - public NativeAssemblerAttribute? GetNativeAssemblerAttribute (MemberInfo mi) - { - if (!assemblerAttributes.TryGetValue (mi, out var attr)) { - assemblerAttributes[mi] = attr = mi.GetCustomAttribute (); - } - return attr; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariable.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariable.cs deleted file mode 100644 index 21eb0c39d6a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariable.cs +++ /dev/null @@ -1,542 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; - -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Flags that control how variables are written in the LLVM IR output. -/// -[Flags] -enum LlvmIrVariableWriteOptions -{ - /// - /// No special write options. - /// - None = 0x0000, - /// - /// Write index comments for array elements. - /// - ArrayWriteIndexComments = 0x0001, - /// - /// Format arrays in rows rather than one element per line. - /// - ArrayFormatInRows = 0x0002, -} - -/// -/// Specifies the number format to use when writing numeric variables. -/// -enum LlvmIrVariableNumberFormat -{ - /// - /// Use the default format for the variable type. - /// - Default, - /// - /// Force hexadecimal format. - /// - Hexadecimal, - /// - /// Force decimal format. - /// - Decimal, -} - -/// -/// Abstract base class for LLVM IR variable references. -/// -abstract class LlvmIrVariableReference -{ - /// - /// Gets a value indicating whether this is a global variable reference. - /// - public abstract bool Global { get; } - /// - /// Gets the prefix character used when referencing this variable in LLVM IR code. - /// - public abstract string NamePrefix { get; } - /// - /// Gets or sets the name of the variable. - /// - public string? Name { get; protected set; } - - /// - /// Returns a string which constitutes a reference to a local (using the % prefix character) or a global - /// (using the @ prefix character) variable, ready for use in the generated code wherever variables are - /// referenced. - /// - public virtual string Reference { - get { - if (String.IsNullOrEmpty (Name)) { - throw new InvalidOperationException ("Variable doesn't have a name, it cannot be referenced"); - } - return $"{NamePrefix}{Name}"; - } - } - /// - /// Initializes a new instance of the class. - /// - /// The name of the variable. - protected LlvmIrVariableReference (string? name) - { - Name = name; - } -} -/// -/// Represents a reference to a global LLVM IR variable. -/// -class LlvmIrGlobalVariableReference : LlvmIrVariableReference -{ - /// - /// Gets a value indicating that this is a global variable reference. - /// - public override bool Global => true; - /// - /// Gets the global variable prefix character '@'. - /// - public override string NamePrefix => "@"; - /// - /// Initializes a new instance of the class. - /// - /// The name of the global variable. - public LlvmIrGlobalVariableReference (string name) - : base (name) - {} -} - -/// -/// Abstract base class for LLVM IR variables that can hold values and be referenced in LLVM IR code. -/// -abstract class LlvmIrVariable : LlvmIrVariableReference, IEquatable -{ - /// - /// Gets or sets the .NET type of this variable. - /// - public Type Type { get; protected set; } - /// - /// Gets or sets the write options that control how this variable is output in LLVM IR. - /// - /// - /// Defaults to . - /// - public LlvmIrVariableWriteOptions WriteOptions { get; set; } = LlvmIrVariableWriteOptions.ArrayWriteIndexComments; - - /// - /// Number of columns an array that is written in rows should have. By default, arrays are written one item in a line, but - /// when the flag is set in , then - /// the value of this property dictates how many items are to be placed in a single row. - /// - public uint ArrayStride { get; set; } = 8; - /// - /// Gets or sets the value stored in this variable. - /// - public object? Value { get; set; } - /// - /// Gets or sets a comment to be written alongside this variable in the LLVM IR output. - /// - public string? Comment { get; set; } - - /// - /// Gets or sets the number format to use when writing numeric values. - /// - /// - /// Defaults to . - /// - public LlvmIrVariableNumberFormat NumberFormat { get; set; } = LlvmIrVariableNumberFormat.Decimal; - - /// - /// Both global and local variables will want their names to matter in equality checks, but function - /// parameters must not take it into account, thus this property. If set to false, - /// will ignore name when checking for equality. - /// - protected bool NameMatters { get; set; } = true; - - /// - /// - /// Certain data must be calculated when the target architecture is known, because it may depend on certain aspects of - /// the target (e.g. its bitness). This callback, if set, will be invoked before the variable is written to the output - /// stream, allowing updating of any such data as described above. - /// - /// - /// First parameter passed to the callback is the variable itself, second parameter is the current - /// and the third is the value previously assigned to - /// - /// - public Action? BeforeWriteCallback { get; set; } - - /// - /// Object passed to the method, if any, as the caller state. - /// - public object? BeforeWriteCallbackCallerState { get; set; } - - /// - /// - /// Callback used when processing array variables, called for each item of the array in order to obtain the item's comment, if any. - /// - /// - /// The first argument is the variable which contains the array, second is the item index, third is the item value and fourth is - /// the caller state object, previously assigned to the property. The callback - /// can return an empty string or null, in which case no comment is written. - /// - /// - public Func? GetArrayItemCommentCallback { get; set; } - - /// - /// Object passed to the method, if any, as the caller state. - /// - public object? GetArrayItemCommentCallbackCallerState { get; set; } - - /// - /// Constructs an abstract variable. is translated to one of the LLVM IR first class types (see - /// https://llvm.org/docs/LangRef.html#t-firstclass) only if it's an integral or floating point type. In all other cases it - /// is treated as an opaque pointer type. - /// - /// The .NET type of the variable. - /// The optional name of the variable. - protected LlvmIrVariable (Type type, string? name = null) - : base (name) - { - Type = type; - Name = name; - } - - /// - /// Returns a hash code for this variable based on its type and name. - /// - /// A hash code for the current object. - public override int GetHashCode () - { - return Type.GetHashCode () ^ (Name?.GetHashCode () ?? 0); - } - - /// - /// Determines whether the specified object is equal to the current variable. - /// - /// The object to compare with the current variable. - /// true if the specified object is equal to the current variable; otherwise, false. - public override bool Equals (object obj) - { - var irVar = obj as LlvmIrVariable; - if (irVar == null) { - return false; - } - - return Equals (irVar); - } - - /// - /// Determines whether the specified LLVM IR variable is equal to the current variable. - /// - /// The LLVM IR variable to compare with the current variable. - /// true if the specified variable is equal to the current variable; otherwise, false. - public virtual bool Equals (LlvmIrVariable other) - { - if (other == null) { - return false; - } - - return - Global == other.Global && - Type == other.Type && - MonoAndroidHelper.StringEquals (NamePrefix, other.NamePrefix) && - (!NameMatters || MonoAndroidHelper.StringEquals (Name, other.Name)); - } -} - -/// -/// Represents a local LLVM IR variable. -/// -class LlvmIrLocalVariable : LlvmIrVariable -{ - /// - /// Gets a value indicating that this is not a global variable. - /// - public override bool Global => false; - /// - /// Gets the local variable prefix character '%'. - /// - public override string NamePrefix => "%"; - - /// - /// Constructs a local variable. is translated to one of the LLVM IR first class types (see - /// https://llvm.org/docs/LangRef.html#t-firstclass) only if it's an integral or floating point type. In all other cases it - /// is treated as an opaque pointer type. is optional because local variables can be unnamed, in - /// which case they will be assigned a sequential number when function code is generated. - /// - /// The .NET type of the variable. - /// The optional name of the variable. - public LlvmIrLocalVariable (Type type, string? name = null) - : base (type, name) - {} - - /// - /// Assigns a numeric name to this local variable. - /// - /// The number to assign as the variable name. - public void AssignNumber (ulong n) - { - Name = n.ToString (CultureInfo.InvariantCulture); - } -} - -/// -/// Represents the state of a streamed array data provider. -/// -enum LlvmIrStreamedArrayDataProviderState -{ - /// - /// More sections are available after this one. - /// - NextSection, - /// - /// This is the last section with data. - /// - LastSection, - /// - /// More sections are available but this one has no data. - /// - NextSectionNoData, - /// - /// This is the last section and it has no data. - /// - LastSectionNoData, -} - -/// -/// Abstract base class for providing streamed array data to LLVM IR variables. -/// -abstract class LlvmIrStreamedArrayDataProvider -{ - /// - /// Type of every member of the array returned by . Generator will check - /// every member type against this property, allowing also derived types. - /// - public Type ArrayElementType { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The type of elements in the arrays returned by this provider. - protected LlvmIrStreamedArrayDataProvider (Type arrayElementType) - { - ArrayElementType = arrayElementType; - } - - /// - /// Whenever returns the generator will call this method to obtain the new section - /// comment, if any, to be output before the actual data. Returning `String.Empty` prevents the comment - /// from being added. - /// - /// The target module for which to get the comment. - /// The section start comment or an empty string if no comment should be added. - public virtual string GetSectionStartComment (LlvmIrModuleTarget target) => String.Empty; - - /// - /// Provide the next chunk of data for the specified target (ABI). Implementations need to return at least one - /// non-empty collection of data. The returned collection **must** be exactly the size of contained data (e.g. it cannot be - /// a byte array rented from a byte pool, because these can be bigger than requested. When returning the last (or the only) section, - /// must have a value of . - /// Each section may be preceded by a comment, . - /// - /// The target module for which to get data. - /// A tuple containing the provider state and the data collection. - public abstract (LlvmIrStreamedArrayDataProviderState status, ICollection data) GetData (LlvmIrModuleTarget target); - - /// - /// Provide the total data size for the specified target (ABI). This needs to be used instead of - /// because a variable instance is created once and shared by all targets, while per-target data sets might have different sizes. - /// - /// The target module for which to get the total data size. - /// The total size of data that will be provided for the specified target. - public abstract ulong GetTotalDataSize (LlvmIrModuleTarget target); -} - -/// -/// Represents a global LLVM IR variable. -/// -class LlvmIrGlobalVariable : LlvmIrVariable -{ - /// - /// By default a global variable is constant and exported. - /// - public static readonly LlvmIrVariableOptions DefaultOptions = LlvmIrVariableOptions.GlobalConstant; - - /// - /// Gets a value indicating that this is a global variable. - /// - public override bool Global => true; - /// - /// Gets the global variable prefix character '@'. - /// - public override string NamePrefix => "@"; - - /// - /// Specify variable options. If omitted, it defaults to . - /// - /// - public virtual LlvmIrVariableOptions? Options { get; set; } - - /// - /// There are situations when a variable differs enough between architectures, that the difference cannot be - /// handled with . In such situations one can create a separate variable - /// for each architecture and set this property. - /// - public AndroidTargetArch? TargetArch { get; set; } - - /// - /// If set to `true`, initialize the array with a shortcut zero-initializer statement. Useful when pre-allocating - /// space for runtime use that won't be filled in with any data at the build time. - /// - public bool ZeroInitializeArray { get; set; } - - /// - /// Specify number of items in an array. Used in cases when we want to pre-allocate an array without giving it any - /// value, thus making it impossible for the generator to discover the number of items automatically. This is useful - /// when using . This property is used **only** if the variable - /// is `null`. - /// - public ulong ArrayItemCount { get; set; } - - /// - /// If set, it will override any automatically calculated alignment for this variable - /// - public ulong? Alignment { get; set; } - - /// - /// If set, the provider will be called to obtain all the data to be placed in an array variable. The total amount - /// of data that will be returned by the provider **must** be specified in the property, - /// in order for the generator to properly declare the variable. The generator will verify that the amount of data - /// is exactly that much and throw an exception otherwise. - /// - public LlvmIrStreamedArrayDataProvider? ArrayDataProvider { get; set; } - - /// - /// Constructs a global variable. is translated to one of the LLVM IR first class types (see - /// https://llvm.org/docs/LangRef.html#t-firstclass) only if it's an integral or floating point type. In all other cases it - /// is treated as an opaque pointer type. is required because global variables must be named. - /// - /// The .NET type of the variable. - /// The name of the global variable. - /// Optional variable options. - /// Thrown when is null or empty. - public LlvmIrGlobalVariable (Type type, string name, LlvmIrVariableOptions? options = null) - : base (type, name) - { - if (String.IsNullOrEmpty (name)) { - throw new ArgumentException ("must not be null or empty", nameof (name)); - } - - Options = options; - } - - /// - /// Constructs a local variable and sets the property to and - /// property to its type. For that reason, **must not** be null. is - /// required because global variables must be named. - /// - /// The value to assign to the variable. Must not be null. - /// The name of the global variable. - /// Optional variable options. - /// Thrown when is null. - public LlvmIrGlobalVariable (object value, string name, LlvmIrVariableOptions? options = null) - : this ((value ?? throw new ArgumentNullException (nameof (value))).GetType (), name, options) - { - Value = value; - } - - /// - /// This is, unfortunately, needed to be able to address scenarios when a single symbol can have a different type when - /// generating output for a specific target (e.g. 32-bit vs 64-bit integer variables). If the variable requires such - /// type changes, this should be done at generation time from within the method. - /// - /// The new type to assign to the variable. - /// The new value to assign to the variable. - public void OverrideTypeAndValue (Type newType, object? newValue) - { - Type = newType; - Value = newValue; - } - - /// - /// Overrides the name of this global variable. - /// - /// The new name for the variable. - public void OverrideName (string newName) - { - Name = newName; - } -} - -/// -/// Represents a string variable in LLVM IR with specific encoding and formatting options. -/// -class LlvmIrStringVariable : LlvmIrGlobalVariable -{ - /// - /// Gets the string encoding used for this variable. - /// - public LlvmIrStringEncoding Encoding { get; } - /// - /// Gets the LLVM IR type string for this string variable. - /// - public string IrType { get; } - /// - /// Gets a value indicating whether this string should be output as a constant string literal. - /// - public bool IsConstantStringLiteral { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the string variable. - /// The string value holder containing the string data and encoding. - /// Optional variable options, defaults to global constant string pointer. - public LlvmIrStringVariable (string name, StringHolder value, LlvmIrVariableOptions? options = null) - : base (typeof(string), name, options ?? LlvmIrVariableOptions.GlobalConstantStringPointer) - { - Value = value; - Encoding = value.Encoding; - - switch (value.Encoding) { - case LlvmIrStringEncoding.UTF8: - IrType = "i8"; - IsConstantStringLiteral = true; - break; - - case LlvmIrStringEncoding.Unicode: - IrType = "i16"; - break; - - default: - throw new InvalidOperationException ($"Internal error: unsupported string encoding {value.Encoding}"); - } - } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the string variable. - /// The string value. - /// The encoding to use for the string. - /// The string comparison method to use. - /// Optional variable options. - public LlvmIrStringVariable (string name, string value, LlvmIrStringEncoding encoding, StringComparison comparison = StringComparison.Ordinal, LlvmIrVariableOptions? options = null) - : this (name, new StringHolder (value, encoding, comparison), options) - {} -} - -/// -/// This is to address my dislike to have single-line variables separated by empty lines :P. -/// When an instance of this "variable" is first encountered, it enables variable grouping, that is -/// they will be followed by just a single newline. The next instance of this "variable" turns -/// grouping off, meaning the following variables will be followed by two newlines. -/// -class LlvmIrGroupDelimiterVariable : LlvmIrGlobalVariable -{ - public LlvmIrGroupDelimiterVariable () - : base (typeof(void), ".:!GroupDelimiter!:.", LlvmIrVariableOptions.LocalConstant) - {} -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariableOptions.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariableOptions.cs deleted file mode 100644 index 775f6eaec0a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrVariableOptions.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Represents options for LLVM IR variables, controlling linkage, visibility, writability, and other attributes. - /// Provides predefined option sets for common variable types. - /// - class LlvmIrVariableOptions - { - /// - /// Options for a global, writable, symbol with locally (module wide) unimportant address - /// - public static readonly LlvmIrVariableOptions Default = new LlvmIrVariableOptions { - Writability = LlvmIrWritability.Writable, - AddressSignificance = LlvmIrAddressSignificance.LocalUnnamed, - }; - - /// - /// Options for a global, read-only, symbol with locally (module wide) unimportant address - /// - public static readonly LlvmIrVariableOptions GlobalConstant = new LlvmIrVariableOptions { - Writability = LlvmIrWritability.Constant, - AddressSignificance = LlvmIrAddressSignificance.LocalUnnamed, - }; - - /// - /// Options for a global, writable, symbol - /// - public static readonly LlvmIrVariableOptions GlobalWritable = new LlvmIrVariableOptions { - Writability = LlvmIrWritability.Writable, - AddressSignificance = LlvmIrAddressSignificance.LocalUnnamed, - }; - - /// - /// Options for a local, read-only, symbol - /// - public static readonly LlvmIrVariableOptions LocalConstant = new LlvmIrVariableOptions { - Linkage = LlvmIrLinkage.Internal, - Writability = LlvmIrWritability.Constant, - }; - - /// - /// Options for a local, writable, symbol - /// - public static readonly LlvmIrVariableOptions LocalWritable = new LlvmIrVariableOptions { - Linkage = LlvmIrLinkage.Internal, - Writability = LlvmIrWritability.Writable, - }; - - /// - /// Options for a local, writable, insignificant address symbol - /// - public static readonly LlvmIrVariableOptions LocalWritableInsignificantAddr = new LlvmIrVariableOptions { - Linkage = LlvmIrLinkage.Internal, - Writability = LlvmIrWritability.Writable, - AddressSignificance = LlvmIrAddressSignificance.Unnamed, - }; - - /// - /// Options for a local, read-only, string which will end up in a strings ELF section - /// - public static readonly LlvmIrVariableOptions LocalString = new LlvmIrVariableOptions { - Linkage = LlvmIrLinkage.Private, - Writability = LlvmIrWritability.Constant, - AddressSignificance = LlvmIrAddressSignificance.Unnamed, - RuntimePreemption = LlvmIrRuntimePreemption.Default, - }; - - /// - /// Options for a local, read-only, C++ constexpr style string which will remain in the rodata ELF section - /// - public static readonly LlvmIrVariableOptions LocalConstexprString = new LlvmIrVariableOptions { - Linkage = LlvmIrLinkage.Internal, - Writability = LlvmIrWritability.Constant, - }; - - /// - /// Options for a global, read-only, C++ constexpr style string which will remain in the rodata ELF section. - /// - public static readonly LlvmIrVariableOptions GlobalConstexprString = new LlvmIrVariableOptions { - Writability = LlvmIrWritability.Constant, - }; - - /// - /// Options for a global, read-only, constant pointer to string - /// - public static readonly LlvmIrVariableOptions GlobalConstantStringPointer = new LlvmIrVariableOptions { - Writability = LlvmIrWritability.Constant, - AddressSignificance = LlvmIrAddressSignificance.LocalUnnamed, - }; - - /// - /// Gets or sets the linkage type for the variable. - /// - public LlvmIrLinkage Linkage { get; set; } = LlvmIrLinkage.Default; - /// - /// Gets or sets the runtime preemption setting for the variable. - /// - public LlvmIrRuntimePreemption RuntimePreemption { get; set; } = LlvmIrRuntimePreemption.DSOLocal; - /// - /// Gets or sets the visibility of the variable. - /// - public LlvmIrVisibility Visibility { get; set; } = LlvmIrVisibility.Default; - /// - /// Gets or sets the address significance of the variable. - /// - public LlvmIrAddressSignificance AddressSignificance { get; set; } = LlvmIrAddressSignificance.Default; - /// - /// Gets or sets whether the variable is writable or constant. - /// - public LlvmIrWritability Writability { get; set; } = LlvmIrWritability.Writable; - - /// - /// Gets a value indicating whether this variable has global linkage. - /// - public bool IsGlobal => Linkage == LlvmIrLinkage.Default || (Linkage != LlvmIrLinkage.Private && Linkage != LlvmIrLinkage.Internal); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrWritability.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrWritability.cs deleted file mode 100644 index 15e32bc64a8..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrWritability.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable enable -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Specifies whether a variable is writable or constant, see https://llvm.org/docs/LangRef.html#global-variables - /// - [Flags] - enum LlvmIrWritability - { - /// - /// Variable is constant and cannot be modified. - /// - Constant = 1 << 0, - /// - /// Variable is writable and can be modified. - /// - Writable = 1 << 1, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmVisibility.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmVisibility.cs deleted file mode 100644 index 3ccc73cf678..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmVisibility.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Visibility style, see https://llvm.org/docs/BitCodeFormat.html#visibility - /// - [Flags] - enum LlvmIrVisibility - { - /// - /// Default visibility - symbol is visible to other modules. - /// - Default = 1 << 0, - /// - /// Hidden visibility - symbol is not visible to other modules. - /// - Hidden = 1 << 1, - /// - /// Protected visibility - symbol is visible but not overrideable. - /// - Protected = 1 << 2, - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/MemberInfoUtilities.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/MemberInfoUtilities.cs deleted file mode 100644 index 5ee1dad4d5d..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/MemberInfoUtilities.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Reflection; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Provides utility methods for working with member information in the context of LLVM IR generation. - /// - static class MemberInfoUtilities - { - /// - /// Determines whether a member is marked as a native pointer. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the member is a native pointer; otherwise, false. - public static bool IsNativePointer (this MemberInfo mi, LlvmIrTypeCache cache) - { - return cache.GetNativePointerAttribute (mi) != null; - } - - /// - /// Determines whether a member is a native pointer that points to a pre-allocated buffer. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// When this method returns, contains the required buffer size if the member points to a pre-allocated buffer; otherwise, 0. - /// true if the member points to a pre-allocated buffer; otherwise, false. - public static bool IsNativePointerToPreallocatedBuffer (this MemberInfo mi, LlvmIrTypeCache cache, out ulong requiredBufferSize) - { - var attr = cache.GetNativePointerAttribute (mi); - if (attr == null) { - requiredBufferSize = 0; - return false; - } - - requiredBufferSize = attr.PreAllocatedBufferSize; - return attr.PointsToPreAllocatedBuffer; - } - - /// - /// Determines whether a member points to a symbol and gets the symbol name. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// When this method returns, contains the symbol name if the member points to a symbol; otherwise, null. - /// true if the member points to a symbol; otherwise, false. - public static bool PointsToSymbol (this MemberInfo mi, LlvmIrTypeCache cache, out string? symbolName) - { - var attr = cache.GetNativePointerAttribute (mi); - if (attr == null || attr.PointsToSymbol == null) { - symbolName = null; - return false; - } - - symbolName = attr.PointsToSymbol; - return true; - } - - /// - /// Determines whether a string member should use Unicode (UTF-16) encoding. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the member should use Unicode encoding; otherwise, false. - public static bool IsUnicodeString (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - if (attr == null) { - return false; - } - - return attr.IsUTF16; - } - - /// - /// Gets the string encoding to use for a member. - /// - /// The member info to get encoding for. - /// The LLVM IR type cache. - /// The string encoding to use for the member. - public static LlvmIrStringEncoding GetStringEncoding (this MemberInfo mi, LlvmIrTypeCache cache) - { - const LlvmIrStringEncoding DefaultStringEncoding = LlvmIrStringEncoding.UTF8; - - var attr = cache.GetNativeAssemblerAttribute (mi); - if (attr == null) { - return DefaultStringEncoding; - } - - return attr.IsUTF16 ? LlvmIrStringEncoding.Unicode : DefaultStringEncoding; - } - - /// - /// Gets the overridden name for a member if specified in attributes. - /// - /// The member info to get the overridden name for. - /// The LLVM IR type cache. - /// The overridden name if specified; otherwise, null. - public static string? GetOverriddenName (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - return attr != null ? attr.MemberName : null; - } - - /// - /// Gets the valid target specification for a member. - /// - /// The member info to get the valid target for. - /// The LLVM IR type cache. - /// The valid target specification for the member. - public static NativeAssemblerValidTarget GetValidTarget (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - return attr != null ? attr.ValidTarget : NativeAssemblerValidTarget.Any; - } - - /// - /// Determines whether a member should be ignored during native assembler generation. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the member should be ignored; otherwise, false. - public static bool ShouldBeIgnored (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - return attr != null && attr.Ignore; - } - - /// - /// Determines whether a member uses a data provider for dynamic data generation. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the member uses a data provider; otherwise, false. - public static bool UsesDataProvider (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - return attr != null && attr.UsesDataProvider; - } - - /// - /// Determines whether a member represents an inline array. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the member is an inline array; otherwise, false. - public static bool IsInlineArray (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - return attr != null && attr.InlineArray; - } - - /// - /// Gets the size of an inline array member. - /// - /// The member info to get the array size for. - /// The LLVM IR type cache. - /// The size of the inline array, or -1 if the member is not an inline array. - public static int GetInlineArraySize (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - if (attr == null || !attr.InlineArray) { - return -1; - } - - return attr.InlineArraySize; - } - - /// - /// Determines whether an inline array member needs padding. - /// - /// The member info to check. - /// The LLVM IR type cache. - /// true if the inline array needs padding; otherwise, false. - public static bool InlineArrayNeedsPadding (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - if (attr == null || !attr.InlineArray) { - return false; - } - - return attr.NeedsPadding; - } - - /// - /// Gets the number format to use for a member. - /// - /// The member info to get the number format for. - /// The LLVM IR type cache. - /// The number format to use for the member. - public static LlvmIrVariableNumberFormat GetNumberFormat (this MemberInfo mi, LlvmIrTypeCache cache) - { - var attr = cache.GetNativeAssemblerAttribute (mi); - if (attr == null) { - return LlvmIrVariableNumberFormat.Default; - } - - return attr.NumberFormat; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerAttribute.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerAttribute.cs deleted file mode 100644 index cb0d6edf3d2..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerAttribute.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks -{ - /// - /// Specifies the target architectures for which a native assembler member is valid. - /// - enum NativeAssemblerValidTarget - { - /// - /// Valid for any target architecture. - /// - Any, - /// - /// Valid only for 32-bit target architectures. - /// - ThirtyTwoBit, - /// - /// Valid only for 64-bit target architectures. - /// - SixtyFourBit, - } - - /// - /// Attribute that controls how fields and properties are handled during native assembler generation. - /// Provides fine-grained control over member inclusion, formatting, and target-specific behavior. - /// - [AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - class NativeAssemblerAttribute : Attribute - { - /// - /// Tells the generator to ignore structure/class member if set to true - /// - public bool Ignore { get; set; } - - /// - /// Indicates that structure/class member uses a data context provider to obtain some data dynamically, - /// based on instances of the given structure/class. - /// - public bool UsesDataProvider { get; set; } - - /// - /// Indicates that the member is an array contained directly in the structure, as opposed to a pointer to array. - /// Size of the array is either specified statically in or by the data context - /// provider's method. - /// - public bool InlineArray { get; set; } - - /// - /// Size of , if set to a positive value. - /// - public int InlineArraySize { get; set; } = -1; - - /// - /// Indicates that the member needs to be padded to a certain size. Maximum array - /// size to which the member must be padded is specified by - /// - public bool NeedsPadding { get; set; } - - public LLVMIR.LlvmIrVariableNumberFormat NumberFormat { get; set; } = LLVMIR.LlvmIrVariableNumberFormat.Default; - - /// - /// Taken into account only for fields of string types. If set to true, the string is output as an UTF16 in - /// the native assembly file. - /// - public bool IsUTF16 { get; set; } - - /// - /// Allows choosing that a structure/class field/property will be considered only for the target of - /// specific bitness. Mainly useful when dealing with hash fields. - /// - public NativeAssemblerValidTarget ValidTarget { get; set; } = NativeAssemblerValidTarget.Any; - - /// - /// Allows overriding of the field name when mapping the structure. This is purely cosmetic, but allows to avoid - /// confusion when dealing with fields/properties that have a different size for 32-bit and 64-bit targets. In such - /// case the structure being mapped will have a separate member per bitness, with the members requiring different names - /// while in reality they have the same name in the native world, regardless of bitness. - /// - /// This field is only considered only when is not set to - /// - public string? MemberName { get; set; } - } - - /// - /// Attribute that specifies a context data provider for a native assembler structure. - /// The provider allows for dynamic data generation based on structure instances. - /// - [AttributeUsage (AttributeTargets.Class, Inherited = true)] - class NativeAssemblerStructContextDataProviderAttribute : Attribute - { - /// - /// Gets the type of the context data provider. - /// - public Type Type { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The type of the context data provider. Must be derived from . - /// Thrown when the type is not derived from . - public NativeAssemblerStructContextDataProviderAttribute (Type type) - { - if (!type.IsSubclassOf (typeof(NativeAssemblerStructContextDataProvider))) { - throw new ArgumentException (nameof (type), "Must be derived from the AssemblerStructContextDataProvider class"); - } - - Type = type; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerContextDataProvider.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerContextDataProvider.cs deleted file mode 100644 index 821ff04aadc..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeAssemblerContextDataProvider.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks -{ - /// - /// Base class for providing context-specific data for native assembler structure generation. - /// Allows dynamic data generation based on structure instances during LLVM IR generation. - /// - class NativeAssemblerStructContextDataProvider - { - /// - /// Return size of a buffer will point to, based on data passed in - /// - /// The structure instance data. - /// The name of the field that needs buffer sizing. - /// The size of the buffer in bytes. - public virtual ulong GetBufferSize (object data, string fieldName) - { - return 0; - } - - /// - /// Return comment for the specified field, based on instance data passed in - /// - /// The structure instance data. - /// The name of the field that needs a comment. - /// The comment string for the field. - public virtual string GetComment (object data, string fieldName) - { - return String.Empty; - } - - /// - /// Get maximum width of data buffer allocated inline (that is as part of structure) - /// - /// The structure instance data. - /// The name of the field that needs inline width information. - /// The maximum inline width in bytes. - public virtual uint GetMaxInlineWidth (object data, string fieldName) - { - return 0; - } - - /// - /// Returns name of the symbol the given field is supposed to point to. null or String.Empty - /// can be returned to make the pointer null - /// - /// The structure instance data. - /// The name of the field that needs symbol name information. - /// The symbol name the field should point to, or null for a null pointer. - public virtual string? GetPointedToSymbolName (object data, string fieldName) - { - return null; - } - - /// - /// Ensures that the provided data object is of the expected type. - /// - /// The expected type of the data object. - /// The data object to validate. - /// The data object cast to the expected type. - /// Thrown when the data object is not of the expected type. - protected T EnsureType (object data) where T: class - { - var ret = data as T; - if (ret == null) { - throw new InvalidOperationException ($"Invalid data type, expected an instance of {typeof(T)}"); - } - - return ret; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeClassAttribute.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeClassAttribute.cs deleted file mode 100644 index d7dfc5f2a4a..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativeClassAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -#nullable enable -using System; - -namespace Xamarin.Android.Tasks -{ - /// - /// Attribute that marks a class as representing a native structure for LLVM IR generation. - /// Classes decorated with this attribute will be treated as native structures during code generation. - /// - [AttributeUsage (AttributeTargets.Class, Inherited = true)] - class NativeClassAttribute : Attribute - { - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativePointerAttribute.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativePointerAttribute.cs deleted file mode 100644 index f46a3983a10..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/NativePointerAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Attribute that marks a field or property as representing a native pointer in LLVM IR generation. - /// This provides control over how the pointer is handled during code generation. - /// - [AttributeUsage (AttributeTargets.Field | AttributeTargets.Property, Inherited = true)] - class NativePointerAttribute : Attribute - { - /// - /// Indicates that the member points to a buffer pre-allocated at the compilation time, with size - /// provided by or, if present, - /// by the property. - /// - public bool PointsToPreAllocatedBuffer { get; set; } - - /// - /// Indicates that the member points to a buffer pre-allocated at the compilation time, with size - /// specified by the value of this property, unless the value is 0 in which case - /// is used. - /// - public ulong PreAllocatedBufferSize { get; set; } = 0; - - /// - /// Indicates the symbol to point to. If null the attribute is ignored, if String.Empty - /// the data context provider is queried (if the type uses it) - /// - public string? PointsToSymbol { get; set; } - - /// - /// A shortcut way to initialize a pointer to null without having to involve context data provider - /// - public bool IsNull { get; set; } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StringHolder.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StringHolder.cs deleted file mode 100644 index a963ec2f4ad..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StringHolder.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; - -namespace Xamarin.Android.Tasks.LLVMIR; - -/// -/// Holds a string with its encoding information and comparison semantics for use in LLVM IR generation. -/// -class StringHolder : IComparable, IComparable, IEquatable -{ - /// - /// Gets the encoding used for this string. - /// - public LlvmIrStringEncoding Encoding { get; } - /// - /// Gets the string data. - /// - public string? Data { get; } - - StringComparison comparison; - - /// - /// Initializes a new instance of the class. - /// - /// The string data to hold. - /// The encoding to use for the string. - /// The string comparison method to use. - public StringHolder (string? data, LlvmIrStringEncoding encoding = LlvmIrStringEncoding.UTF8, StringComparison comparison = StringComparison.Ordinal) - { - Data = data; - Encoding = encoding; - this.comparison = comparison; - } - - /// - /// Converts an object to a StringHolder, handling both string and StringHolder inputs. - /// - /// The value to convert (string or StringHolder). - /// The encoding to use if creating a new StringHolder. - /// The comparison method to use if creating a new StringHolder. - /// A StringHolder containing the value. - /// Thrown when value is not a string or StringHolder. - public static StringHolder AsHolder (object? value, LlvmIrStringEncoding encoding = LlvmIrStringEncoding.UTF8, StringComparison comparison = StringComparison.Ordinal) - { - if (value == null) { - return new StringHolder ((string?)value); - } - - StringHolder holder; - if (value is string) { - holder = new StringHolder ((string)value, encoding, comparison); - } else if (value is StringHolder) { - holder = (StringHolder)value; - } else { - throw new InvalidOperationException ($"Internal error: expected 'string' type, got '{value.GetType ()}' instead."); - } - - return holder; - } - - /// - /// Compares this StringHolder to an object. - /// - /// The object to compare to. - /// A signed integer that indicates the relative values of this instance and value. - public int CompareTo (object obj) => CompareTo (obj as StringHolder); - - /// - /// Compares this StringHolder to another StringHolder. - /// - /// The StringHolder to compare to. - /// A signed integer that indicates the relative values of this instance and other. - public int CompareTo (StringHolder? other) - { - if (other == null) { - return 1; - } - - int encodingCompare = Encoding.CompareTo (other.Encoding); - if (Data == null) { - if (other.Data != null) { - // We are "smaller", because the other holder actually has a valid string - return -1; - } - - // Both strings are null, so we care only about the encoding - return encodingCompare; - } - - int dataCompare = other.Data == null ? Data.CompareTo (other.Data) : String.Compare (Data, other.Data, comparison); - // If encodings are identical, we compare strings, allowing any result of the comparisons - if (encodingCompare == 0) { - return dataCompare; - } - - // However if encodings aren't the same, we mustn't allow Data comparison to return 0 - // as the strings are **not** equal, even though their Data values are. In this case, - // we fall back to encoding for comparison result. - return dataCompare != 0 ? dataCompare : encodingCompare; - } - - /// - /// Returns a hash code for this StringHolder. - /// - /// A hash code for the current object. - public override int GetHashCode () - { - int hc = 0; - if (Data != null) { - hc ^= Data.GetHashCode (); - } - - return hc ^ Encoding.GetHashCode (); - } - - /// - /// Determines whether the specified object is equal to the current StringHolder. - /// - /// The object to compare with the current StringHolder. - /// true if the specified object is equal to the current StringHolder; otherwise, false. - public override bool Equals (object obj) => Equals (obj as StringHolder); - - /// - /// Determines whether the specified StringHolder is equal to the current StringHolder. - /// - /// The StringHolder to compare with the current StringHolder. - /// true if the specified StringHolder is equal to the current StringHolder; otherwise, false. - public bool Equals (StringHolder? other) - { - if (other == null || Encoding != other.Encoding) { - return false; - } - - return MonoAndroidHelper.StringEquals (Data, other.Data, comparison); - } - - /// - /// Determines whether one StringHolder is greater than another. - /// - /// The first StringHolder to compare. - /// The second StringHolder to compare. - /// true if a is greater than b; otherwise, false. - public static bool operator > (StringHolder a, StringHolder b) - { - return a.CompareTo (b) > 0; - } - - /// - /// Determines whether one StringHolder is less than another. - /// - /// The first StringHolder to compare. - /// The second StringHolder to compare. - /// true if a is less than b; otherwise, false. - public static bool operator < (StringHolder a, StringHolder b) - { - return a.CompareTo (b) < 0; - } - - /// - /// Determines whether one StringHolder is greater than or equal to another. - /// - /// The first StringHolder to compare. - /// The second StringHolder to compare. - /// true if a is greater than or equal to b; otherwise, false. - public static bool operator >= (StringHolder a, StringHolder b) - { - return a.CompareTo (b) >= 0; - } - - /// - /// Determines whether one StringHolder is less than or equal to another. - /// - /// The first StringHolder to compare. - /// The second StringHolder to compare. - /// true if a is less than or equal to b; otherwise, false. - public static bool operator <= (StringHolder a, StringHolder b) - { - return a.CompareTo (b) <= 0; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInfo.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInfo.cs deleted file mode 100644 index 5a1600086dc..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInfo.cs +++ /dev/null @@ -1,103 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - // TODO: add cache for members and data provider info - sealed class StructureInfo - { - Type type; - readonly LlvmIrTypeCache cache; - - public Type Type => type; - public string Name { get; } = String.Empty; - public ulong Size { get; } - public IList Members { get; } = new List (); - public NativeAssemblerStructContextDataProvider? DataProvider { get; } - public ulong MaxFieldAlignment { get; private set; } = 0; - public bool HasStrings { get; private set; } - public bool HasPreAllocatedBuffers { get; private set; } - public bool HasPointers { get; private set; } - - public bool IsOpaque => Members.Count == 0; - public string NativeTypeDesignator { get; } - - public StructureInfo (LlvmIrModule module, Type type, LlvmIrTypeCache cache) - { - this.type = type; - this.cache = cache; - Name = type.GetShortName (); - Size = GatherMembers (type, module); - DataProvider = type.GetDataProvider (); - NativeTypeDesignator = type.IsNativeClass () ? "class" : "struct"; - } - - public string? GetCommentFromProvider (StructureMemberInfo smi, StructureInstance instance) - { - if (DataProvider == null || !smi.Info.UsesDataProvider (cache)) { - return null; - } - - string ret = DataProvider.GetComment (instance.Obj, smi.Info.Name); - if (ret.Length == 0) { - return null; - } - - return ret; - } - - public ulong GetBufferSizeFromProvider (StructureMemberInfo smi, StructureInstance instance) - { - if (DataProvider == null) { - return 0; - } - - return DataProvider.GetBufferSize (instance.Obj, smi.Info.Name); - } - - ulong GatherMembers (Type type, LlvmIrModule module, bool storeMembers = true) - { - ulong size = 0; - foreach (MemberInfo mi in type.GetMembers ()) { - if (mi.ShouldBeIgnored (cache) || (!(mi is FieldInfo) && !(mi is PropertyInfo))) { - continue; - } - - var info = new StructureMemberInfo (mi, module, cache); - if (info.IsNativePointer) { - HasPointers = true; - } - - if (storeMembers) { - Members.Add (info); - size += info.Size; - - if (info.Alignment > MaxFieldAlignment) { - MaxFieldAlignment = info.Alignment; - } - } - - if (!HasStrings && info.MemberType == typeof (string)) { - HasStrings = true; - } - - if (!HasPreAllocatedBuffers && info.Info.IsNativePointerToPreallocatedBuffer (cache, out ulong _)) { - HasPreAllocatedBuffers = true; - } - - // If we encounter an embedded struct (as opposed to a pointer), we need to descend and check if that struct contains any strings or buffers, but we - // do NOT want to store any members while doing that, as the struct should have been mapped by the composer previously. - // The presence of strings/buffers is important at the generation time as it is used to decide whether we need separate stream writers for them and - // if the owning structure does **not** have any of those, the generated code would be invalid - if (info.IsIRStruct) { - GatherMembers (info.MemberType, module, storeMembers: false); - } - } - - return size; - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInstance.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInstance.cs deleted file mode 100644 index f5bdf4874b8..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureInstance.cs +++ /dev/null @@ -1,96 +0,0 @@ -#nullable disable - -using System; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - enum StructureInstanceType - { - /// - /// Instance describes actual data to be output. Guarantees that the - /// property isn't `null`. - /// - Data, - - /// - /// Instance marks the beginning of section in an array of - /// structures. Ignored if we're not outputting an array of structures. - /// property will **always** be `null` - /// in such instances. - /// - ArraySection, - } - - abstract class StructureInstance - { - StructureInfo info; - - public object? Obj { get; } - public Type Type => info.Type; - public StructureInfo Info => info; - public StructureInstanceType InstanceType { get; } - public string? Comment { get; } - - /// - /// Do **not** set this property, it is used internally by , - /// and when dealing with arrays of objects where each - /// array index contains the same object instance - /// - internal ulong IndexInArray { get; set; } - - /// - /// This is a cludge to support zero-initialized structures. In order to output proper variable type - /// when a structure is used, the generator must be able to read the structure descrption, which is - /// provided in the property and, thus, it requires a variable of structural type to - /// **always** have a non-null value. To support zero initialization of such structures, this property - /// can be set to true - /// - public bool IsZeroInitialized { get; set; } - - protected StructureInstance (StructureInfo info, object instance, string? comment = null) - { - if (instance == null) { - throw new ArgumentNullException (nameof (instance)); - } - - InstanceType = StructureInstanceType.Data; - if (!info.Type.IsAssignableFrom (instance.GetType ())) { - throw new ArgumentException ($"must be an instance of, or derived from, the {info.Type} type, or `null` (was {instance})", nameof (instance)); - } - - this.info = info; - Obj = instance; - Comment = comment; - } - - protected StructureInstance (StructureInfo info, string? comment = null) - { - InstanceType = StructureInstanceType.ArraySection; - this.info = info; - Comment = comment; - } - } - - /// - /// Represents a typed structure instance, derived from the class. The slightly weird - /// approach is because on one hand we need to operate on a heterogenous set of structures (in which generic types would - /// only get in the way), but on the other hand we need to be able to get the structure type (whose instance is in - /// and ) only by looking at the **type**. This is needed in situations when we have - /// an array of some structures that is empty - we wouldn't be able to gleam the structure type from any instance and we still - /// need to output a strongly typed LLVM IR declaration of the structure array. With this class, most of the code will use the - /// abstract type, and knowing we have only one non-abstract implementation of the class allows - /// us to use StructureInstance<T> in a cast, to get T via reflection. - /// - sealed class StructureInstance : StructureInstance - { - public T? Instance => (T)Obj; - - public StructureInstance (StructureInfo info, T instance, string? comment = null) - : base (info, instance, comment) - {} - - public StructureInstance (StructureInfo info, string? comment = null) - : base (info, comment) - {} - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureMemberInfo.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureMemberInfo.cs deleted file mode 100644 index dafdb2da10f..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/StructureMemberInfo.cs +++ /dev/null @@ -1,163 +0,0 @@ -#nullable disable - -using System; -using System.Reflection; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - sealed class StructureMemberInfo - { - public string IRType { get; } - public MemberInfo Info { get; } - public Type MemberType { get; } - - /// - /// Size of a variable with this IR type. May differ from because the field - /// can be a pointer to type or a struct - /// - public ulong Size { get; } - public ulong Alignment { get; } - public ulong ArrayElements { get; } - - /// - /// Size of the member's base IR type. If the variable is a pointer, this property will represent - /// the size of a base type, not the pointer. - /// - public ulong BaseTypeSize { get; } - public bool IsNativePointer { get; } - public bool IsNativeArray { get; } - public bool IsInlineArray { get; } - public bool NeedsPadding { get; } - - /// - /// Whether this member is written out as an LLVM IR structure, as opposed to a simple - /// value, a string or a pointer. Computed once during construction: it depends only on - /// the member's type and attributes, both of which are fixed, while the value is queried - /// once per member for *every* structure instance being generated. - /// - public bool IsIRStruct { get; } - - /// - /// Some fields/properties may override their name for presentation purposes, . - /// In such instances, this property will return the overridden/mapped name, otherwise it returns . - /// This property should be used only for "presentation" purposes - for instance when generating comments which refer to - /// native field names. - /// - public string MappedName { - get { - string? name = Info.GetOverriddenName (typeCache); - return String.IsNullOrEmpty (name) ? Info.Name : name; - } - } - - readonly LlvmIrTypeCache typeCache; - - public StructureMemberInfo (MemberInfo mi, LlvmIrModule module, LlvmIrTypeCache cache) - { - Info = mi; - typeCache = cache; - - MemberType = mi switch { - FieldInfo fi => fi.FieldType, - PropertyInfo pi => pi.PropertyType, - _ => throw new InvalidOperationException ($"Unsupported member type {mi}") - }; - - // MemberType.IsStructure () handles checks for primitive types, enums etc - IsIRStruct = - MemberType != typeof(string) && - !mi.IsInlineArray (cache) && - !mi.IsNativePointer (cache) && - (MemberType.IsStructure () || MemberType.IsClass); - - ulong size = 0; - bool isPointer = false; - if (MemberType != typeof(string) && !MemberType.IsArray && (MemberType.IsStructure () || MemberType.IsClass)) { - IRType = $"%struct.{MemberType.GetShortName ()}"; - // TODO: figure out how to get structure size if it isn't a pointer - } else { - IRType = LlvmIrGenerator.MapToIRType (MemberType, cache, out size, out isPointer); - } - IsNativePointer = isPointer; - - if (!IsNativePointer) { - IsNativePointer = mi.IsNativePointer (cache); - if (IsNativePointer) { - IRType = LlvmIrGenerator.IRPointerType; - } - } - - BaseTypeSize = size; - ArrayElements = 0; - IsInlineArray = false; - NeedsPadding = false; - Alignment = 0; - - if (IsNativePointer) { - size = 0; // Real size will be determined when code is generated and we know the target architecture - } else if (mi.IsInlineArray (cache)) { - if (!MemberType.IsArray) { - throw new InvalidOperationException ($"Internal error: member {mi.Name} of structure {mi.DeclaringType.Name} is marked as inline array, but is not of an array type."); - } - - IsInlineArray = true; - IsNativeArray = true; - NeedsPadding = mi.InlineArrayNeedsPadding (cache); - int arrayElements = mi.GetInlineArraySize (cache); - if (arrayElements < 0) { - arrayElements = GetArraySizeFromProvider (MemberType.GetDataProvider (), mi.Name); - } - - if (arrayElements < 0) { - throw new InvalidOperationException ($"Array cannot have negative size (got {arrayElements})"); - } - - IRType = $"[{arrayElements} x {IRType}]"; - ArrayElements = (ulong)arrayElements; - } else if (IsIRStruct) { - StructureInfo si = module.GetStructureInfo (MemberType); - size = si.Size; - Alignment = (ulong)si.MaxFieldAlignment; - } - - if (MemberType.IsArray && !IsInlineArray) { - throw new InvalidOperationException ("Out of line arrays in structures aren't currently supported"); - } - - Size = size; - if (Alignment == 0) { - Alignment = size; - } - } - - public bool IsSupportedForTarget (LlvmIrModuleTarget target) - { - NativeAssemblerValidTarget validTarget = Info.GetValidTarget (typeCache); - return validTarget switch { - NativeAssemblerValidTarget.Any => true, - NativeAssemblerValidTarget.ThirtyTwoBit => !target.Is64Bit, - NativeAssemblerValidTarget.SixtyFourBit => target.Is64Bit, - _ => throw new NotSupportedException ($"Internal error: unsupported native assembler valid target value: {validTarget}") - }; - } - - public object? GetValue (object instance) - { - if (Info is FieldInfo fi) { - return fi.GetValue (instance); - } - - var pi = Info as PropertyInfo; - return pi.GetValue (instance); - } - - int GetArraySizeFromProvider (NativeAssemblerStructContextDataProvider? provider, string fieldName) - { - if (provider == null) { - return -1; - } - - return (int)provider.GetMaxInlineWidth (null, fieldName); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/TypeUtilities.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/TypeUtilities.cs deleted file mode 100644 index f265b8df95c..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/TypeUtilities.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Reflection; - -namespace Xamarin.Android.Tasks.LLVMIR -{ - /// - /// Provides utility methods for working with types in the context of LLVM IR generation. - /// - static class TypeUtilities - { - /// - /// Gets the short name of a type (without namespace or containing type information). - /// - /// The type to get the short name for. - /// The short name of the type. - /// Thrown when the type is unnamed or has an invalid name. - public static string GetShortName (this Type type) - { - string? fullName = type.FullName; - - if (String.IsNullOrEmpty (fullName)) { - throw new InvalidOperationException ($"Unnamed types aren't supported ({type})"); - } - - int lastCharIdx = fullName.LastIndexOf ('.'); - string ret; - if (lastCharIdx < 0) { - ret = fullName; - } else { - ret = fullName.Substring (lastCharIdx + 1); - } - - lastCharIdx = ret.LastIndexOf ('+'); - if (lastCharIdx >= 0) { - ret = ret.Substring (lastCharIdx + 1); - } - - if (String.IsNullOrEmpty (ret)) { - throw new InvalidOperationException ($"Invalid type name ({type})"); - } - - return ret; - } - - /// - /// Determines whether a type represents a structure (value type that is not an enum, primitive, or array). - /// - /// The type to check. - /// true if the type is a structure; otherwise, false. - public static bool IsStructure (this Type type) - { - return type.IsValueType && - !type.IsEnum && - !type.IsPrimitive && - !type.IsArray && - type != typeof (decimal) && - type != typeof (DateTime) && - type != typeof (object); - } - - /// - /// Gets the data provider for a type if it has a NativeAssemblerStructContextDataProviderAttribute. - /// - /// The type to get the data provider for. - /// The data provider instance, or null if the type doesn't have a data provider attribute. - public static NativeAssemblerStructContextDataProvider? GetDataProvider (this Type t) - { - var attr = t.GetCustomAttribute (); - if (attr == null) { - return null; - } - - return Activator.CreateInstance (attr.Type) as NativeAssemblerStructContextDataProvider; - } - - /// - /// Determines whether a type is marked with the NativeClassAttribute. - /// - /// The type to check. - /// true if the type has the NativeClassAttribute; otherwise, false. - public static bool IsNativeClass (this Type t) - { - var attr = t.GetCustomAttribute (); - return attr != null; - } - - /// - /// Determines whether a type implements a specific interface. - /// - /// The type to check. - /// The interface type to check for. - /// true if the type implements the interface; otherwise, false. - public static bool ImplementsInterface (this Type type, Type requiredIfaceType) - { - if (type == null || requiredIfaceType == null) { - return false; - } - - if (type == requiredIfaceType) { - return true; - } - - bool generic = requiredIfaceType.IsGenericType; - foreach (Type iface in type.GetInterfaces ()) { - if (iface == requiredIfaceType) { - return true; - } - - if (generic) { - if (!iface.IsGenericType) { - continue; - } - - if (iface.GetGenericTypeDefinition () == requiredIfaceType.GetGenericTypeDefinition ()) { - return true; - } - } - } - - return false; - } - - /// - /// Determines whether a type is a StructureInstance generic type and gets the structure type. - /// - /// The type to check. - /// When this method returns, contains the structure type if the type is a StructureInstance; otherwise, null. - /// true if the type is a StructureInstance; otherwise, false. - public static bool IsStructureInstance (this Type type, out Type? structureType) - { - structureType = null; - if (!type.IsGenericType) { - return false; - } - - if (type.GetGenericTypeDefinition () != typeof(StructureInstance<>)) { - return false; - } - - structureType = type.GetGenericArguments ()[0]; - return true; - } - - /// - /// Return element type of a single-dimensional (with one exception, see below) array. Parameter **MUST** - /// correspond to one of the following array types: T[], ICollection<T> or IDictionary<string, string>. The latter is - /// used to comfortably represent name:value arrays, which are output as single dimensional arrays in the native code. - /// - /// The array type to get the element type for. - /// The element type of the array. - /// - /// Thrown when is not one of the array types listed above. - /// - public static Type GetArrayElementType (this Type type) - { - if (type.IsArray) { - return type.GetElementType (); - } - - if (!type.IsGenericType) { - throw WrongTypeException (); - } - - Type genericType = type.GetGenericTypeDefinition (); - if (genericType.ImplementsInterface (typeof(ICollection<>))) { - Type[] genericArgs = type.GetGenericArguments (); - return genericArgs[0]; - } - - if (!genericType.ImplementsInterface (typeof(IDictionary))) { - throw WrongTypeException (); - } - - return typeof(string); - - // Dictionary - Exception WrongTypeException () => new InvalidOperationException ($"Internal error: type '{type}' is not an array, ICollection or IDictionary"); - } - - /// - /// Determine whether type represents an array, in our understanding. That means the type has to be - /// a standard single-dimensional language array (i.e. T[]), implement ICollection<T> together with ICollection or, - /// as a special case for name:value pair collections, implement IDictionary<string, string> - /// - /// The type to check. - /// true if the type represents an array; otherwise, false. - /// Thrown when multi-dimensional arrays are encountered. - public static bool IsArray (this Type t) - { - if (t.IsPrimitive) { - return false; - } - - if (t == typeof(string)) { - return false; - } - - if (t.IsArray) { - if (t.GetArrayRank () > 1) { - throw new NotSupportedException ("Internal error: multi-dimensional arrays aren't supported"); - } - - return true; - } - - // TODO: cache results here - // IDictionary is a special case for name:value string arrays which we use for some constructs. - return (t.ImplementsInterface (typeof(ICollection<>)) || t.ImplementsInterface (typeof(ICollection))) || - t.ImplementsInterface (typeof(IDictionary)); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrHelpers.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrHelpers.cs deleted file mode 100644 index d78f44bccb4..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrHelpers.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; - -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Utilities; -using Xamarin.Android.Tasks.LLVMIR; - -namespace Xamarin.Android.Tasks; - -class LlvmIrHelpers -{ - sealed class AppEnvironmentVariableContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var envVar = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("name_index", fieldName)) { - return $" '{envVar.Name}'"; - } - - if (MonoAndroidHelper.StringEquals ("value_index", fieldName)) { - return $" '{envVar.Value}'"; - } - - return String.Empty; - } - } - - // Order of fields and their type must correspond *exactly* to that in - // src/native/clr/include/xamarin-app.hh AppEnvironmentVariable structure - [NativeAssemblerStructContextDataProvider (typeof (AppEnvironmentVariableContextDataProvider))] - internal sealed class AppEnvironmentVariable - { - [NativeAssembler (Ignore = true)] - public string? Name; - - [NativeAssembler (Ignore = true)] - public string? Value; - - [NativeAssembler (UsesDataProvider = true)] - public uint name_index; - - [NativeAssembler (UsesDataProvider = true)] - public uint value_index; - } - - public static void DeclareDummyFunction (LlvmIrModule module, LlvmIrGlobalVariableReference symref) - { - if (symref.Name.IsNullOrEmpty ()) { - throw new InvalidOperationException ("Internal error: variable reference must have a name"); - } - - // Just a dummy declaration, we don't care about the arguments - var funcSig = new LlvmIrFunctionSignature (symref.Name!, returnType: typeof(void)); - var _ = module.DeclareExternalFunction (funcSig); - } - - public static List> MakeEnvironmentVariableList ( - TaskLoggingHelper log, - IDictionary? environmentVariables, - LlvmIrStringBlob envVarsBlob, - StructureInfo? appEnvironmentVariableStructureInfo) - { - var ret = new List> (); - if (environmentVariables == null || environmentVariables.Count == 0) { - return ret; - } - - foreach (var kvp in environmentVariables) { - string? name = kvp.Key.Trim (); - if (String.IsNullOrEmpty (name)) { - log.LogDebugMessage ($"Not adding environment variable without a name. Value: '{kvp.Value}'"); - continue; - } - (int nameOffset, int _) = envVarsBlob.Add (name); - (int valueOffset, int _) = envVarsBlob.Add (kvp.Value); - - var appEnvVar = new LlvmIrHelpers.AppEnvironmentVariable { - Name = name, - Value = kvp.Value, - - name_index = (uint)nameOffset, - value_index = (uint)valueOffset, - }; - ret.Add (new StructureInstance (appEnvironmentVariableStructureInfo, appEnvVar)); - } - - return ret; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrTarget.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrTarget.cs new file mode 100644 index 00000000000..7d1b45b5192 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrTarget.cs @@ -0,0 +1,106 @@ +#nullable enable +using System; + +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +/// +/// Per-ABI constants used when writing textual LLVM IR modules. The data layouts and triples +/// are the ones used by the Android NDK clang for the respective targets. +/// +sealed class LlvmIrTarget +{ + public static readonly LlvmIrTarget Arm = new ( + AndroidTargetArch.Arm, + triple: "armv7-unknown-linux-android21", + dataLayout: "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64", + pointerSize: 4, + moduleFlags: [ + "i32 1, !\"min_enum_size\", i32 4", + ] + ); + + public static readonly LlvmIrTarget Arm64 = new ( + AndroidTargetArch.Arm64, + triple: "aarch64-unknown-linux-android21", + dataLayout: "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128", + pointerSize: 8, + moduleFlags: [ + "i32 1, !\"branch-target-enforcement\", i32 0", + "i32 1, !\"sign-return-address\", i32 0", + "i32 1, !\"sign-return-address-all\", i32 0", + "i32 1, !\"sign-return-address-with-bkey\", i32 0", + ] + ); + + public static readonly LlvmIrTarget X86 = new ( + AndroidTargetArch.X86, + triple: "i686-unknown-linux-android21", + dataLayout: "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128", + pointerSize: 4, + moduleFlags: [ + "i32 1, !\"NumRegisterParameters\", i32 0", + ] + ); + + public static readonly LlvmIrTarget X86_64 = new ( + AndroidTargetArch.X86_64, + triple: "x86_64-unknown-linux-android21", + dataLayout: "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128", + pointerSize: 8, + moduleFlags: [] + ); + + public AndroidTargetArch Arch { get; } + public string Triple { get; } + public string DataLayout { get; } + public uint PointerSize { get; } + public bool Is64Bit => PointerSize == 8; + + /// + /// Target-specific entries appended to the !llvm.module.flags metadata. + /// + public string[] ModuleFlags { get; } + + LlvmIrTarget (AndroidTargetArch arch, string triple, string dataLayout, uint pointerSize, string[] moduleFlags) + { + Arch = arch; + Triple = triple; + DataLayout = dataLayout; + PointerSize = pointerSize; + ModuleFlags = moduleFlags; + } + + public static LlvmIrTarget Get (AndroidTargetArch arch) + { + return arch switch { + AndroidTargetArch.Arm => Arm, + AndroidTargetArch.Arm64 => Arm64, + AndroidTargetArch.X86 => X86, + AndroidTargetArch.X86_64 => X86_64, + _ => throw new InvalidOperationException ($"Unsupported Android target ABI {arch}") + }; + } + + /// + /// Returns the alignment of an aggregate (array or structure) whose most strictly aligned + /// field requires and whose data is + /// bytes long. For arrays of structures, and for structures, is computed + /// using the sum of sizes of all the non-pointer structure members (i.e. excluding any padding and + /// pointers), in order to produce the same symbol alignment the native code has always been built with. + /// + public ulong GetAggregateAlignment (ulong maxFieldAlignment, ulong dataSize) + { + // System V ABI for x86_64 mandates that any aggregates 16 bytes or more long will + // be aligned at at least 16 bytes + // + // See: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf (Section '3.1.2 Data Representation', "Aggregates and Unions") + // + if (Arch == AndroidTargetArch.X86_64 && dataSize >= 16 && maxFieldAlignment < 16) { + return 16; + } + + return maxFieldAlignment; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs new file mode 100644 index 00000000000..62883cffa6d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs @@ -0,0 +1,414 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +/// +/// Minimal helper used by the native assembly generators to write textual LLVM IR. The generators +/// write the IR text themselves (mostly as interpolated raw string literals), this class merely +/// takes care of the parts shared by all of them: the module header and metadata footer, optional +/// comments, string literals and alignment of global symbols. +/// +sealed class LlvmIrWriter +{ + // Symbol attributes, see https://llvm.org/docs/LangRef.html#global-variables + public const string GlobalConstant = "dso_local local_unnamed_addr constant"; + public const string GlobalWritable = "dso_local local_unnamed_addr global"; + public const string LocalConstant = "internal dso_local constant"; + public const string LocalString = "private unnamed_addr constant"; + + readonly TextWriter output; + + public LlvmIrTarget Target { get; } + + /// + /// Whether descriptive comments are written to the generated LLVM IR. Comments make the + /// output easier to read, but they can account for the majority of the file's size and + /// have no effect on the code llc produces. + /// + public bool EmitComments { get; } + + public LlvmIrWriter (TextWriter output, LlvmIrTarget target, bool emitComments) + { + this.output = output ?? throw new ArgumentNullException (nameof (output)); + Target = target ?? throw new ArgumentNullException (nameof (target)); + EmitComments = emitComments; + } + + /// + /// Writes verbatim, except that line breaks (which may come from the + /// C# source files the raw string literals live in) are written using the output's newline + /// sequence. + /// + public void Write (string text) + { + string newLine = output.NewLine; + if (text.IndexOf ('\n') < 0 || (MonoAndroidHelper.StringEquals ("\n", newLine) && text.IndexOf ('\r') < 0)) { + output.Write (text); + return; + } + + int start = 0; + while (start < text.Length) { + int eol = text.IndexOf ('\n', start); + if (eol < 0) { + output.Write (text.Substring (start)); + break; + } + + int end = eol > start && text [eol - 1] == '\r' ? eol - 1 : eol; + output.Write (text.Substring (start, end - start)); + output.Write (newLine); + start = eol + 1; + } + } + + public void WriteLine (string text = "") + { + Write (text); + output.WriteLine (); + } + + /// + /// Returns as an LLVM IR comment (i.e. prefixed with ;) if comments are + /// enabled, an empty string otherwise. The text is expected to contain any leading whitespace. + /// + public string Comment (string? text) => EmitComments && !text.IsNullOrEmpty () ? $";{SanitizeComment (text)}" : ""; + + /// + /// Returns a comment which follows a value on the same line (separated with a space), if comments are enabled. + /// + public string TrailingComment (string? text) => EmitComments && !text.IsNullOrEmpty () ? $" ;{SanitizeComment (text)}" : ""; + + public void WriteCommentLine (string? text) + { + if (!EmitComments || text.IsNullOrEmpty ()) { + return; + } + + WriteLine ($";{SanitizeComment (text)}"); + } + + // Comments end at the end of line, a line break in e.g. an environment variable value would + // turn the rest of the comment into (invalid) IR code + static string SanitizeComment (string text) + { + if (text.IndexOf ('\n') < 0 && text.IndexOf ('\r') < 0) { + return text; + } + + return text.Replace ("\r", "\\r").Replace ("\n", "\\n"); + } + + public void WriteHeader (string fileName) + { + string name = Path.GetFileName (fileName); + + WriteCommentLine ($" ModuleID = '{name}'"); + Write ($$""" + source_filename = "{{name}}" + target datalayout = "{{Target.DataLayout}}" + target triple = "{{Target.Triple}}" + + """); + } + + /// + /// Writes a global symbol definition. may span several lines. + /// + public void WriteGlobal (string name, string attributes, string type, string value, ulong alignment, string? comment = null) + { + WriteLine (); + WriteCommentLine (comment); + WriteLine ($"@{name} = {attributes} {type} {value}, align {Number (alignment)}"); + } + + /// + /// Renders an array initializer in which every element is written on its own line. Elements must + /// already be indented. may return a comment which is + /// placed after the element when comments are enabled. + /// + public string ArrayValue (IList elements, Func? getElementComment = null) + { + if (elements.Count == 0) { + return "zeroinitializer"; + } + + var sb = new StringBuilder (); + sb.Append ("[\n"); + for (int i = 0; i < elements.Count; i++) { + if (i > 0) { + sb.Append (','); + AppendCommentOrNewline (i - 1); + } + sb.Append (elements [i]); + } + AppendCommentOrNewline (elements.Count - 1); + sb.Append (']'); + + return sb.ToString (); + + void AppendCommentOrNewline (int index) + { + sb.Append (getElementComment != null ? TrailingComment (getElementComment (index)) : ""); + sb.Append ('\n'); + } + } + + /// + /// Returns the conventional array element comment, i.e. the element index. + /// + public static string IndexComment (int index) => $" {Number (index)}"; + + /// + /// Alignment of a structure or an array with elements of the given size and alignment. + /// + public ulong GetAggregateAlignment (ulong maxFieldAlignment, ulong dataSize) => Target.GetAggregateAlignment (maxFieldAlignment, dataSize); + + public ulong GetPointerArrayAlignment (int count) => GetAggregateAlignment (Target.PointerSize, (ulong)count * Target.PointerSize); + + /// + /// Writes the module metadata (module flags, identification and the type based alias analysis + /// descriptors), which ends every module. + /// + public void WriteMetadata () + { + var flags = new StringBuilder ("!0, !1"); + var targetFlags = new StringBuilder (); + for (int i = 0; i < Target.ModuleFlags.Length; i++) { + int n = i + 7; + flags.Append ($", !{Number (n)}"); + targetFlags.Append ($"!{Number (n)} = !{{{Target.ModuleFlags [i]}}}\n"); + } + + WriteLine (); + WriteCommentLine (" Metadata"); + Write ($$""" + !llvm.module.flags = !{{{flags}}} + !0 = !{i32 1, !"wchar_size", i32 4} + !1 = !{i32 7, !"PIC Level", i32 2} + !llvm.ident = !{!2} + !2 = !{!".NET for Android {{XABuildConfig.XamarinAndroidBranch}} @ {{XABuildConfig.XamarinAndroidCommitHash}}"} + !3 = !{!4, !4, i64 0} + !4 = !{!"any pointer", !5, i64 0} + !5 = !{!"omnipotent char", !6, i64 0} + !6 = !{!"Simple C++ TBAA"} + + """); + Write (targetFlags.ToString ()); + } + + /// + /// Writes declarations of external functions. All of them use the same, dummy, signature since + /// we only care about being able to take their address. + /// + public void WriteExternalFunctionDeclarations (ICollection names) + { + if (names.Count == 0) { + return; + } + + var sorted = new List (names); + sorted.Sort ((string a, string b) => a.CompareTo (b)); + + WriteLine (); + Write (Comment (" External functions")); + foreach (string name in sorted) { + WriteLine (); + Write ($"declare void @{name}() local_unnamed_addr"); + } + } + + public static string Number (int value) => value.ToString (CultureInfo.InvariantCulture); + public static string Number (uint value) => value.ToString (CultureInfo.InvariantCulture); + public static string Number (ulong value) => value.ToString (CultureInfo.InvariantCulture); + + public static string Hex (uint value) => $"u0x{value.ToString ("x8", CultureInfo.InvariantCulture)}"; + + public static string Bool (bool value) => value ? "true" : "false"; + + /// + /// Renders as an LLVM IR string constant (c"..."), escaping all the + /// characters which aren't printable ASCII as well as " and \. + /// + public static string QuoteBytes (byte[] bytes, bool nullTerminated) + { + var sb = new StringBuilder (bytes.Length + 8); + sb.Append ("c\""); + AppendEscaped (sb, bytes); + if (nullTerminated) { + sb.Append ("\\00"); + } + sb.Append ('"'); + + return sb.ToString (); + } + + public static void AppendEscaped (StringBuilder sb, byte[] bytes) + { + foreach (byte b in bytes) { + if (b != (byte)'"' && b != (byte)'\\' && b >= 32 && b < 127) { + sb.Append ((char)b); + continue; + } + + sb.Append ('\\'); + sb.Append (b.ToString ("X2", CultureInfo.InvariantCulture)); + } + } +} + +/// +/// Collects NUL-terminated UTF-8 strings which are written out as a single byte array, with the +/// strings referred to by their offset into the array. Identical strings are stored only once. +/// +sealed class LlvmIrStringBlob +{ + readonly Dictionary offsets = new (StringComparer.Ordinal); + readonly List segments = new (); + + public ulong Size { get; private set; } + + public int Add (string s) + { + if (offsets.TryGetValue (s, out int offset)) { + return offset; + } + + byte[] bytes = MonoAndroidHelper.Utf8StringToBytes (s); + offset = (int)Size; + segments.Add (bytes); + offsets.Add (s, offset); + Size += (ulong)bytes.Length + 1; + + return offset; + } + + public string Type => $"[{LlvmIrWriter.Number (Size)} x i8]"; + + public string Value () + { + var sb = new StringBuilder ((int)Size + 2); + sb.Append ("c\""); + foreach (byte[] bytes in segments) { + LlvmIrWriter.AppendEscaped (sb, bytes); + sb.Append ("\\00"); + } + sb.Append ('"'); + + return sb.ToString (); + } + + public void Write (LlvmIrWriter writer, string name) + { + writer.WriteGlobal (name, LlvmIrWriter.GlobalConstant, Type, Value (), writer.GetAggregateAlignment (1, Size)); + } +} + +/// +/// Private string literals referred to by pointers stored in global symbols. Every unique string +/// is emitted once, into the group used when it was first requested. Strings are written at the +/// end of the module, grouped, in the order in which they were first requested. +/// +sealed class LlvmIrStringPool +{ + sealed class Group + { + public readonly string SymbolPrefix; + public readonly string? Comment; + public readonly List<(string Symbol, string Value)> Strings = new (); + + public Group (string symbolPrefix, string? comment) + { + SymbolPrefix = symbolPrefix; + Comment = comment; + } + } + + const string DefaultGroupName = "str"; + + readonly Dictionary symbols = new (StringComparer.Ordinal); + readonly Dictionary groupsByName = new (StringComparer.Ordinal); + readonly List groups = new (); + + public bool IsEmpty => symbols.Count == 0; + + /// + /// Returns name of the symbol which contains . Symbols in the default + /// group are named .str.N, those in named groups .{group}.N_{suffix}. + /// + public string GetSymbol (string value, string? groupName = null, string? symbolSuffix = null) + { + if (symbols.TryGetValue (value, out string? existing)) { + return existing; + } + + if (groups.Count == 0) { + AddGroup (DefaultGroupName, comment: null); + } + + if (groupName.IsNullOrEmpty ()) { + groupName = DefaultGroupName; + } + + if (!groupsByName.TryGetValue (groupName, out Group? group)) { + group = AddGroup (groupName, groupName); + } + + string symbol = $"{group.SymbolPrefix}.{LlvmIrWriter.Number (group.Strings.Count)}"; + if (!symbolSuffix.IsNullOrEmpty ()) { + symbol = $"{symbol}_{symbolSuffix}"; + } + group.Strings.Add ((symbol, value)); + symbols.Add (value, symbol); + + return symbol; + } + + /// + /// Returns a pointer to the string, or null if is null. + /// + public string GetPointer (string? value, string? groupName = null, string? symbolSuffix = null) + { + if (value == null) { + return "null"; + } + + return $"@{GetSymbol (value, groupName, symbolSuffix)}"; + } + + Group AddGroup (string name, string? comment) + { + var group = new Group ($".{name}", comment); + groups.Add (group); + groupsByName.Add (name, group); + + return group; + } + + public void Write (LlvmIrWriter writer) + { + if (IsEmpty) { + return; + } + + writer.WriteLine (); + writer.Write (writer.Comment (" Strings")); + foreach (Group group in groups) { + writer.WriteLine (); + writer.WriteCommentLine (group.Comment); + + foreach ((string symbol, string value) in group.Strings) { + byte[] bytes = MonoAndroidHelper.Utf8StringToBytes (value); + ulong size = (ulong)bytes.Length + 1; + writer.WriteLine ($"@{symbol} = {LlvmIrWriter.LocalString} [{LlvmIrWriter.Number (size)} x i8] {LlvmIrWriter.QuoteBytes (bytes, nullTerminated: true)}, align {LlvmIrWriter.Number (writer.GetAggregateAlignment (1, size))}"); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs index 210512de964..49f3aaf4f6a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs @@ -1,77 +1,47 @@ +#nullable enable using System; using System.Collections.Generic; +using System.IO; + using Microsoft.Build.Utilities; -using Xamarin.Android.Tasks.LLVMIR; +using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; -class NativeAotEnvironmentNativeAssemblyGenerator : LlvmIrComposer +class NativeAotEnvironmentNativeAssemblyGenerator { - readonly EnvironmentBuilder envBuilder; - StructureInfo? appEnvironmentVariableStructureInfo; + readonly AppEnvironmentVariableTable environmentVariables; + readonly AppEnvironmentVariableTable systemProperties; - public NativeAotEnvironmentNativeAssemblyGenerator (TaskLoggingHelper log, EnvironmentBuilder envBuilder) - : base (log) - { - this.envBuilder = envBuilder; - } + /// + /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// + public bool EmitComments { get; set; } - protected override void Construct (LlvmIrModule module) + public NativeAotEnvironmentNativeAssemblyGenerator (TaskLoggingHelper log, EnvironmentBuilder envBuilder) { - MapStructures (module); - - SortedDictionary? environmentVariables = null; - if (envBuilder.EnvironmentVariables.Count > 0) { - environmentVariables = new (envBuilder.EnvironmentVariables, StringComparer.Ordinal); - } - - SortedDictionary? systemProperties = null; - if (envBuilder.SystemProperties.Count > 0) { - systemProperties = new (envBuilder.SystemProperties, StringComparer.Ordinal); - } else { - systemProperties = new (StringComparer.Ordinal); + if (log == null) { + throw new ArgumentNullException (nameof (log)); } - var envVarsBlob = new LlvmIrStringBlob (); - List> appEnvVars = LlvmIrHelpers.MakeEnvironmentVariableList ( - Log, - environmentVariables, - envVarsBlob, - appEnvironmentVariableStructureInfo - ); + environmentVariables = new AppEnvironmentVariableTable (log, new SortedDictionary (envBuilder.EnvironmentVariables, StringComparer.Ordinal)); + systemProperties = new AppEnvironmentVariableTable (log, new SortedDictionary (envBuilder.SystemProperties, StringComparer.Ordinal)); + } - var envVarsCount = new LlvmIrGlobalVariable ((uint)appEnvVars.Count, "__naot_android_app_environment_variable_count"); - module.Add (envVarsCount); + public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) + { + var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + w.WriteHeader (fileName); + AppEnvironmentVariableTable.WriteDeclaration (w); - var envVars = new LlvmIrGlobalVariable (appEnvVars, "__naot_android_app_environment_variables") { - Comment = " Application environment variables array, name:value", - Options = LlvmIrVariableOptions.GlobalConstant, - }; - module.Add (envVars); - module.AddGlobalVariable ("__naot_android_app_environment_variable_contents", envVarsBlob, LlvmIrVariableOptions.GlobalConstant); + w.WriteGlobal ("__naot_android_app_environment_variable_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (environmentVariables.Count), 4); + environmentVariables.Write (w, "__naot_android_app_environment_variables", "__naot_android_app_environment_variable_contents", " Application environment variables array, name:value"); + w.WriteGlobal ("__naot_android_app_system_property_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (systemProperties.Count), 4); // We reuse the same structure as for environment variables, there's no point in adding a new, identical, one - var sysPropsBlob = new LlvmIrStringBlob (); - List> appSysProps = LlvmIrHelpers.MakeEnvironmentVariableList ( - Log, - systemProperties, - sysPropsBlob, - appEnvironmentVariableStructureInfo - ); - - var sysPropsCount = new LlvmIrGlobalVariable ((uint)appSysProps.Count, "__naot_android_app_system_property_count"); - module.Add (sysPropsCount); + systemProperties.Write (w, "__naot_android_app_system_properties", "__naot_android_app_system_property_contents", " System properties defined by the application"); - var sysProps = new LlvmIrGlobalVariable (appSysProps, "__naot_android_app_system_properties") { - Comment = " System properties defined by the application", - Options = LlvmIrVariableOptions.GlobalConstant, - }; - module.Add (sysProps); - module.AddGlobalVariable ("__naot_android_app_system_property_contents", sysPropsBlob, LlvmIrVariableOptions.GlobalConstant); - } - - void MapStructures (LlvmIrModule module) - { - appEnvironmentVariableStructureInfo = module.MapStructure (); + w.WriteMetadata (); + output.Flush (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs index a865295d8d9..d222081c578 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs @@ -1,33 +1,34 @@ +#nullable enable using System; using System.Collections.Generic; +using System.IO; using Microsoft.Build.Utilities; using Xamarin.Android.Tasks; -using Xamarin.Android.Tasks.LLVMIR; +using Xamarin.Android.Tools; namespace Xamarin.Android.Build.Tasks; -class NativeAotJniInitNativeAssemblyGenerator : LlvmIrComposer +class NativeAotJniInitNativeAssemblyGenerator { - readonly List? runtimeComponentsJniOnLoadHandlers; - readonly List? customJniOnLoadHandlers; + readonly List jniOnLoadNames = new (); + + /// + /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// + public bool EmitComments { get; set; } public NativeAotJniInitNativeAssemblyGenerator (TaskLoggingHelper log, List? runtimeComponentsJniOnLoadHandlers, List? customJniOnLoadHandlers) - : base (log) { - this.runtimeComponentsJniOnLoadHandlers = runtimeComponentsJniOnLoadHandlers; - this.customJniOnLoadHandlers = customJniOnLoadHandlers; - } + if (log == null) { + throw new ArgumentNullException (nameof (log)); + } - protected override void Construct (LlvmIrModule module) - { - var jniOnLoadNames = new List (); var seenNames = new HashSet (StringComparer.Ordinal); // We call BCL/runtime handlers first, to make sure user libraries can rely on them being initialized (just in case) CollectHandlers (runtimeComponentsJniOnLoadHandlers); CollectHandlers (customJniOnLoadHandlers); - JniOnLoadNativeAssemblerHelper.GenerateJniOnLoadHandlerCode (jniOnLoadNames, module); void CollectHandlers (List? handlers) { @@ -36,12 +37,38 @@ void CollectHandlers (List? handlers) } foreach (string name in handlers) { - if (seenNames.Contains (name)) { - continue; + if (seenNames.Add (name)) { + jniOnLoadNames.Add (name); } - seenNames.Add (name); - jniOnLoadNames.Add (name); } } } + + public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) + { + var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + var strings = new LlvmIrStringPool (); + + w.WriteHeader (fileName); + w.WriteGlobal ("__jni_on_load_handler_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (jniOnLoadNames.Count), 4); + + var handlers = new List (jniOnLoadNames.Count); + var names = new List (jniOnLoadNames.Count); + foreach (string name in jniOnLoadNames) { + handlers.Add ($"\tptr @{name}"); + names.Add ($"\tptr {strings.GetPointer (name)}"); + } + + string type = $"[{LlvmIrWriter.Number (handlers.Count)} x ptr]"; + ulong alignment = w.GetPointerArrayAlignment (handlers.Count); + w.WriteGlobal ("__jni_on_load_handlers", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (handlers, LlvmIrWriter.IndexComment), alignment); + w.WriteGlobal ("__jni_on_load_handler_names", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (names, i => $" {LlvmIrWriter.Number (i)} ('{jniOnLoadNames [i]}')"), alignment); + + strings.Write (w); + + // All the handlers are declared with the same dummy signature, we only need to take their address + w.WriteExternalFunctionDeclarations (jniOnLoadNames); + w.WriteMetadata (); + output.Flush (); + } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs index 26f08c2e2eb..9720601731e 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeRuntimeComponents.cs @@ -1,148 +1,55 @@ #nullable enable -using System; using System.Collections.Generic; -using Microsoft.Build.Framework; - namespace Xamarin.Android.Tasks; +/// +/// Static archives of the native runtime components, used by NativeAOT builds to find +/// the JNI initialization functions of BCL components used by the application. +/// class NativeRuntimeComponents { - public sealed class KnownSets - { - public const string BCL = "bcl"; - public const string CoreClrRuntime = "coreclr"; - public const string CompilerRuntime = "compiler-rt"; - public const string XamarinAndroidRuntime = "xaruntime"; - } - - internal class Archive + internal sealed class Archive { public readonly string Name; public readonly string? JniOnLoadName; - public bool Include => shouldInclude (this); - public readonly bool WholeArchive; - public bool DontExportSymbols { get; set; } - public HashSet? SymbolsToPreserve { get; set; } - public string SetName { get; } - public readonly bool NeedsClrHack; - - Func shouldInclude; - - public Archive (string name, string setName, Func? include = null, bool wholeArchive = false, string? jniOnLoadName = null) + public Archive (string name, string? jniOnLoadName = null) { Name = name; - SetName = setName; - shouldInclude = include ?? (_ => true); - WholeArchive = wholeArchive; JniOnLoadName = jniOnLoadName; } } - sealed class ClangBuiltinsArchive : Archive - { - public ClangBuiltinsArchive (string clangAbi) - : base ($"libclang_rt.builtins-{clangAbi}-android.a", KnownSets.CompilerRuntime) - {} - } - - class AndroidArchive : Archive - { - public AndroidArchive (string name, bool wholeArchive = false) - : base (name, KnownSets.XamarinAndroidRuntime, wholeArchive: wholeArchive) - {} - } - - sealed class BclArchive : Archive - { - public BclArchive (string name, bool wholeArchive = false, string? jniOnLoadName = null) - : base (name, KnownSets.BCL, wholeArchive: wholeArchive, jniOnLoadName: jniOnLoadName) - { - DontExportSymbols = true; - } - } - - sealed class ClrArchive : Archive - { - public ClrArchive (string name, bool wholeArchive = false) - : base (name, KnownSets.CoreClrRuntime, wholeArchive: wholeArchive) - { - DontExportSymbols = true; - } - } - - readonly ITaskItem[]? monoComponents; - public readonly List KnownArchives; - public readonly List NativeLibraries; - public readonly List LinkStartFiles; - public readonly List LinkEndFiles; - public NativeRuntimeComponents (ITaskItem[]? monoComponents) + public NativeRuntimeComponents () { - this.monoComponents = monoComponents; KnownArchives = new () { // CoreCLR runtime + BCL - new ClrArchive ("libcoreclr_static.a"), - new ClrArchive ("libbrotlienc.a"), - new ClrArchive ("libbrotlidec.a"), - new ClrArchive ("libbrotlicommon.a"), + new Archive ("libcoreclr_static.a"), + new Archive ("libbrotlienc.a"), + new Archive ("libbrotlidec.a"), + new Archive ("libbrotlicommon.a"), - new BclArchive ("libSystem.Globalization.Native.a"), - new BclArchive ("libSystem.IO.Compression.Native.a"), - new BclArchive ("libSystem.Native.a"), - new BclArchive ("libSystem.Security.Cryptography.Native.Android.a", jniOnLoadName: "AndroidCryptoNative_InitLibraryOnLoad") { - SymbolsToPreserve = new (StringComparer.Ordinal) { - // This isn't referenced directly by any code in libSystem.Security.Cryptography.Native.Android. It is instead - // referenced by the Java code shipped with the component (`DotnetProxyTrustManager`), as a native Java method: - // - // static native boolean verifyRemoteCertificate(long sslStreamProxyHandle); - // - // Therefore we must reference it explicitly - "Java_net_dot_android_crypto_DotnetProxyTrustManager_verifyRemoteCertificate" - }, - - // For now, we have to export all the symbols from this archive because we need the above `Java_net*` symbol to be - // externally visible, and the linker's `--exclude-libs` flag works on the archive (.a) level. - // - // TODO: use `llvm-ar` to extract the relevant object file and link it separately? - DontExportSymbols = false, - }, + new Archive ("libSystem.Globalization.Native.a"), + new Archive ("libSystem.IO.Compression.Native.a"), + new Archive ("libSystem.Native.a"), + new Archive ("libSystem.Security.Cryptography.Native.Android.a", jniOnLoadName: "AndroidCryptoNative_InitLibraryOnLoad"), // .NET for Android - new AndroidArchive ("libnet-android.release-static-release.a", wholeArchive: true), - new AndroidArchive ("libruntime-base-common-release.a"), - new AndroidArchive ("libruntime-base-release.a"), - new AndroidArchive ("libxa-java-interop-release.a"), - new AndroidArchive ("libxa-shared-bits-release.a"), - new AndroidArchive ("libxamarin-startup-release.a"), + new Archive ("libnet-android.release-static-release.a"), + new Archive ("libruntime-base-common-release.a"), + new Archive ("libruntime-base-release.a"), + new Archive ("libxa-java-interop-release.a"), + new Archive ("libxa-shared-bits-release.a"), + new Archive ("libxamarin-startup-release.a"), // LLVM clang built-ins archives - new ClangBuiltinsArchive ("aarch64"), - new ClangBuiltinsArchive ("arm"), - new ClangBuiltinsArchive ("i686"), - new ClangBuiltinsArchive ("x86_64"), - }; - - // Just the base names of libraries to link into the unified runtime. Must have all the dependencies of all the static archives we - // link into the final library. - NativeLibraries = new () { - "c", - "dl", - "log", - "m", - "z", - }; - - // Files that will be linked before any other object/archive/library files - LinkStartFiles = new () { - "crtbegin_so.o", - }; - - // Files that will be linked after any other object/archive/library files - LinkEndFiles = new () { - "crtend_so.o", + new Archive ("libclang_rt.builtins-aarch64-android.a"), + new Archive ("libclang_rt.builtins-arm-android.a"), + new Archive ("libclang_rt.builtins-i686-android.a"), + new Archive ("libclang_rt.builtins-x86_64-android.a"), }; } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs deleted file mode 100644 index 251fd345d66..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/PreservePinvokesNativeAssemblyGenerator.cs +++ /dev/null @@ -1,372 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; - -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -using Xamarin.Android.Tasks.LLVMIR; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks; - -class PreservePinvokesNativeAssemblyGenerator : LlvmIrComposer -{ - sealed class PInvoke - { - public readonly LlvmIrFunction NativeFunction; - public readonly uint Hash; - - public PInvoke (LlvmIrModule module, PinvokeScanner.PinvokeEntryInfo pinfo) - { - Hash = TypeMapHelper.HashNameForCLR (pinfo.EntryName); - - // All the p/invoke functions use the same dummy signature. The only thing we care about is - // a way to reference to the symbol at build time so that we can return pointer to it. For - // that all we need is a known name, signature doesn't matter to us. - var funcSig = new LlvmIrFunctionSignature (name: pinfo.EntryName, returnType: typeof(void)); - NativeFunction = module.DeclareExternalFunction (funcSig); - } - } - - sealed class Component - { - public readonly string Name; - public readonly uint NameHash; - public readonly List PInvokes; - - public Component (string name) - { - Name = name; - NameHash = TypeMapHelper.HashNameForCLR (name); - PInvokes = new (); - } - - public void Add (LlvmIrModule module, PinvokeScanner.PinvokeEntryInfo pinfo) - { - PInvokes.Add (new PInvoke (module, pinfo)); - } - - public void Sort () - { - PInvokes.Sort ((PInvoke a, PInvoke b) => a.Hash.CompareTo (b.Hash)); - } - } - - sealed class ConstructionState - { - public LlvmIrFunction Func = null!; - public LlvmIrFunctionLabelItem ReturnLabel = null!; - public LlvmIrFunctionParameter EntryPointHashParam = null!; - public LlvmIrInstructions.Phi Phi = null!; - } - - // Maps a component name after ridding it of the `lib` prefix and the extension to a "canonical" - // name of a library, as used in `[DllImport]` attributes. - readonly Dictionary libraryNameMap = new (StringComparer.Ordinal) { - { "xa-java-interop", "java-interop" }, - { "mono-android.release-static", String.Empty }, - { "mono-android.release", String.Empty }, - }; - - readonly AndroidTargetArch targetArch; - readonly List pinvokeInfos; - readonly ITaskItem[] monoComponents; - - public PreservePinvokesNativeAssemblyGenerator (TaskLoggingHelper log, AndroidTargetArch targetArch, List pinvokeInfos, ITaskItem[] monoComponents) - : base (log) - { - this.targetArch = targetArch; - this.pinvokeInfos = pinvokeInfos ?? throw new ArgumentNullException (nameof (pinvokeInfos)); - this.monoComponents = monoComponents; - } - - protected override void Construct (LlvmIrModule module) - { - Log.LogDebugMessage ($"[{targetArch}] Constructing p/invoke preserve code"); - if (pinvokeInfos.Count == 0) { - // This is a very unlikely scenario, but we will work just fine. The module that this generator produces will merely result - // in an empty (but valid) .ll file and an "empty" object file to link into the shared library. - return; - } - - Log.LogDebugMessage (" Looking for enabled native components"); - var componentNames = new HashSet (StringComparer.Ordinal); - var componentLoadHandlers = new Dictionary (StringComparer.Ordinal); - var componentPreservedSymbols = new Dictionary> (StringComparer.Ordinal); - var nativeComponents = new NativeRuntimeComponents (monoComponents); - foreach (NativeRuntimeComponents.Archive archiveItem in nativeComponents.KnownArchives) { - if (!archiveItem.Include) { - continue; - } - - Log.LogDebugMessage ($" {archiveItem.Name}"); - componentNames.Add (archiveItem.Name); - if (!archiveItem.JniOnLoadName.IsNullOrEmpty ()) { - componentLoadHandlers.Add (archiveItem.Name, archiveItem.JniOnLoadName!); - } - - if (archiveItem.SymbolsToPreserve == null || archiveItem.SymbolsToPreserve.Count == 0) { - continue; - } - - var preservedSymbols = new HashSet (); - foreach (string symbolName in archiveItem.SymbolsToPreserve) { - preservedSymbols.Add (new LlvmIrGlobalVariableReference (symbolName)); - } - componentPreservedSymbols.Add (archiveItem.Name, preservedSymbols); - } - - if (componentNames.Count == 0) { - Log.LogDebugMessage ("No native framework components are included in the build, not scanning for p/invoke usage"); - return; - } - - switch (targetArch) { - case AndroidTargetArch.Arm64: - case AndroidTargetArch.X86_64: - case AndroidTargetArch.Arm: - case AndroidTargetArch.X86: - break; - default: - throw new NotSupportedException ($"Architecture {targetArch} is not supported here"); - } - - Log.LogDebugMessage (" Checking discovered p/invokes against the list of components"); - var preservedPerComponent = new Dictionary (StringComparer.OrdinalIgnoreCase); - var processedCache = new HashSet (StringComparer.OrdinalIgnoreCase); - var jniOnLoadNames = new HashSet (StringComparer.Ordinal); - bool haveLoadHandlers = componentLoadHandlers.Count > 0; - bool havePreservedSymbols = componentPreservedSymbols.Count > 0; - - var symbolsToExplicitlyPreserve = new HashSet (); - - foreach (PinvokeScanner.PinvokeEntryInfo pinfo in pinvokeInfos) { - Log.LogDebugMessage ($" p/invoke: {pinfo.EntryName} in {pinfo.LibraryName}"); - string key = $"{pinfo.LibraryName}/${pinfo.EntryName}"; - if (processedCache.Contains (key)) { - Log.LogDebugMessage ($" already processed"); - continue; - } - - processedCache.Add (key); - (bool preserve, string? componentName) = MustPreserve (pinfo, componentNames); - if (!preserve) { - Log.LogDebugMessage (" no need to preserve"); - continue; - } - Log.LogDebugMessage (" must be preserved"); - - if (!componentName.IsNullOrEmpty ()) { - if (haveLoadHandlers && componentLoadHandlers.TryGetValue (componentName!, out string jniOnLoadName)) { - if (jniOnLoadNames.Add (jniOnLoadName)) { - Log.LogDebugMessage ($" component '{componentName}' registers a load handler '{jniOnLoadName}'"); - } - } - - if (havePreservedSymbols && componentPreservedSymbols.TryGetValue (componentName!, out HashSet preservedSymbols)) { - foreach (LlvmIrGlobalVariableReference vref in preservedSymbols) { - LlvmIrHelpers.DeclareDummyFunction (module, vref); - symbolsToExplicitlyPreserve.Add (vref); - } - componentPreservedSymbols.Remove (componentName!); - } - } - - if (!preservedPerComponent.TryGetValue (pinfo.LibraryName, out Component? component)) { - component = new Component (pinfo.LibraryName); - preservedPerComponent.Add (component.Name, component); - } - component.Add (module, pinfo); - } - - JniOnLoadNativeAssemblerHelper.GenerateJniOnLoadHandlerCode (jniOnLoadNames, module); - module.AddGlobalVariable ("__explicitly_preserved_symbols", symbolsToExplicitlyPreserve, LlvmIrVariableOptions.GlobalConstant); - var components = new List (preservedPerComponent.Values); - AddFindPinvoke (module, components); - } - - void AddFindPinvoke (LlvmIrModule module, List components) - { - var parameters = new List { - new LlvmIrFunctionParameter (typeof(uint), "library_name_hash") { - NoUndef = true, - }, - - new LlvmIrFunctionParameter (typeof(uint), "entrypoint_hash") { - NoUndef = true, - }, - - new LlvmIrFunctionParameter (typeof(IntPtr), "known_library") { - Align = 1, // it's a reference to C++ `bool` - Dereferenceable = 1, - IsCplusPlusReference = true, - NoCapture = true, - NonNull = true, - NoUndef = true, - WriteOnly = true, - }, - }; - - var sig = new LlvmIrFunctionSignature ( - name: "find_pinvoke", - returnType: typeof(IntPtr), - parameters: parameters, - new LlvmIrFunctionSignature.ReturnTypeAttributes { - NoUndef = true, - } - ); - - var func = new LlvmIrFunction (sig, MakeFindPinvokeAttributeSet (module)) { - CallingConvention = LlvmIrCallingConvention.Fastcc, - Visibility = LlvmIrVisibility.Hidden, - }; - LlvmIrLocalVariable retval = func.CreateLocalVariable (typeof(IntPtr), "retval"); - var state = new ConstructionState { - Func = func, - ReturnLabel = new LlvmIrFunctionLabelItem ("return"), - EntryPointHashParam = parameters[1], - Phi = new LlvmIrInstructions.Phi (retval), - }; - module.Add (state.Func); - state.Func.Body.Add (new LlvmIrFunctionLabelItem ("entry")); - - var libraryNameSwitchEpilog = new LlvmIrFunctionLabelItem ("libNameSW.epilog"); - var componentSwitch = new LlvmIrInstructions.Switch (parameters[0], libraryNameSwitchEpilog, "sw.libname"); - - state.Func.Body.Add (componentSwitch); - state.Phi.AddNode (libraryNameSwitchEpilog, null); - - components.Sort ((Component a, Component b) => a.NameHash.CompareTo (b.NameHash)); - Log.LogDebugMessage (" Components to be preserved:"); - uint componentID = 1; - - foreach (Component component in components) { - Log.LogDebugMessage ($" {component.Name} (hash: 0x{component.NameHash:x}; {component.PInvokes.Count} p/invoke(s))"); - - string comment = $" {component.Name} (p/invoke count: {component.PInvokes.Count})"; - LlvmIrFunctionLabelItem componentLabel = AddSwitchItem (componentSwitch, component.NameHash, comment, null); - - func.Body.Add (componentLabel, comment); - AddPInvokeSwitch (state, componentLabel, component, componentID++); - } - - func.Body.Add (libraryNameSwitchEpilog); - - var setKnownLib = new LlvmIrInstructions.Store (false, parameters[2]); - func.Body.Add (setKnownLib); - AddReturnBranch (func, state.ReturnLabel); - - func.Body.Add (state.ReturnLabel); - func.Body.Add (state.Phi); - func.Body.Add (new LlvmIrInstructions.Ret (typeof (IntPtr), retval)); - } - - void AddPInvokeSwitch (ConstructionState state, LlvmIrFunctionLabelItem componentLabel, Component component, uint id) - { - var pinvokeSwitchEpilog = new LlvmIrFunctionLabelItem ($"pinvokeSW.epilog.{id}"); - state.Phi.AddNode (pinvokeSwitchEpilog, null); - - var pinvokeSwitch = new LlvmIrInstructions.Switch (state.EntryPointHashParam, pinvokeSwitchEpilog, $"sw.pinvoke.{id}"); - state.Func.Body.Add (pinvokeSwitch); - - component.Sort (); - bool first = true; - foreach (PInvoke pi in component.PInvokes) { - string pinvokeName = pi.NativeFunction.Signature.Name; - string comment = $" {pinvokeName}"; - LlvmIrFunctionLabelItem pinvokeLabel = AddSwitchItem (pinvokeSwitch, pi.Hash, comment, first ? state.ReturnLabel : null); - - // First item of every component switch block "reuses" the block's label - if (first) { - first = false; - } else { - state.Func.Body.Add (pinvokeLabel, comment); - AddReturnBranch (state.Func, state.ReturnLabel); - } - - state.Phi.AddNode (pinvokeLabel == state.ReturnLabel ? componentLabel : pinvokeLabel, new LlvmIrGlobalVariableReference (pinvokeName)); - } - - state.Func.Body.Add (pinvokeSwitchEpilog); - AddReturnBranch (state.Func, state.ReturnLabel); - } - - void AddReturnBranch (LlvmIrFunction func, LlvmIrFunctionLabelItem returnLabel) - { - var branch = new LlvmIrInstructions.Br (returnLabel); - func.Body.Add (branch); - } - - LlvmIrFunctionLabelItem AddSwitchItem (LlvmIrInstructions.Switch sw, uint hash, string? comment, LlvmIrFunctionLabelItem? label) - { - return sw.Add (hash, dest: label, comment: comment); - } - - LlvmIrFunctionAttributeSet MakeFindPinvokeAttributeSet (LlvmIrModule module) - { - var attrSet = new LlvmIrFunctionAttributeSet { - new MustprogressFunctionAttribute (), - new NofreeFunctionAttribute (), - new NorecurseFunctionAttribute (), - new NosyncFunctionAttribute (), - new NounwindFunctionAttribute (), - new WillreturnFunctionAttribute (), - new MemoryFunctionAttribute { - Default = MemoryAttributeAccessKind.Write, - Argmem = MemoryAttributeAccessKind.None, - InaccessibleMem = MemoryAttributeAccessKind.None, - }, - new UwtableFunctionAttribute (), - new NoTrappingMathFunctionAttribute (true), - }; - - return module.AddAttributeSet (attrSet); - } - - // Returns `true` for all p/invokes that we know are part of our set of components, otherwise returns `false`. - // Returning `false` merely means that the p/invoke isn't in any of BCL or our code and therefore we shouldn't - // care. It doesn't mean the p/invoke will be removed in any way. - (bool preserve, string? componentName) MustPreserve (PinvokeScanner.PinvokeEntryInfo pinfo, ICollection components) - { - if (MonoAndroidHelper.StringEquals ("xa-internal-api", pinfo.LibraryName, StringComparison.Ordinal)) { - return (true, null); - } - - foreach (string component in components) { - // The most common pattern for the BCL - file name without extension - string componentName = Path.GetFileNameWithoutExtension (component); - if (Matches (pinfo.LibraryName, componentName)) { - return (true, componentName); - } - - // If it starts with `lib`, drop the prefix - if (componentName.StartsWith ("lib", StringComparison.Ordinal)) { - if (Matches (pinfo.LibraryName, componentName.Substring (3))) { - return (true, componentName); - } - } - - // Might require mapping of component name to a canonical one - if (libraryNameMap.TryGetValue (componentName, out string? mappedComponentName) && !mappedComponentName.IsNullOrEmpty ()) { - if (Matches (pinfo.LibraryName, mappedComponentName)) { - return (true, componentName); - } - } - - // Try full file name, as the last resort - if (Matches (pinfo.LibraryName, Path.GetFileName (component))) { - return (true, componentName); - } - } - - return (false, null); - - bool Matches (string libraryName, string componentName) - { - return MonoAndroidHelper.StringEquals (libraryName, componentName, StringComparison.Ordinal); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 5a54c3d1c1e..aef387cbfcc 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -59,7 +59,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - @@ -1505,14 +1504,6 @@ because xbuild doesn't support framework reference assemblies. Mode="compressed"> - - - @@ -1673,18 +1664,9 @@ because xbuild doesn't support framework reference assemblies. > - - - - @@ -1812,7 +1794,6 @@ because xbuild doesn't support framework reference assemblies. <_CompileToDalvikDependsOnTargets> _CompileJava; _CreateApplicationSharedLibraries; - $(_NativeRuntimeLinking); $(_BeforeCompileToDalvik); _GetLibraryImports; _SetProguardMappingFileProperty; @@ -1907,9 +1888,6 @@ because xbuild doesn't support framework reference assemblies. <_NativeAssemblyTarget Include="@(_AndroidRemapAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')"> %(_AndroidRemapAssemblySource.abi) - <_NativeAssemblyTarget Include="@(_RuntimeLinkingAssemblySource->'$([System.IO.Path]::ChangeExtension('%(Identity)', '.o'))')"> - %(_RuntimeLinkingAssemblySource.abi) - @@ -1931,10 +1909,10 @@ because xbuild doesn't support framework reference assemblies. + Outputs="@(_ApplicationSharedLibrary)"> Date: Wed, 23 Sep 2026 13:20:33 +0200 Subject: [PATCH 38/58] Inline LLVM IR structure comments and numeric formatting Keep static structure comments directly in IR literals and render numeric values under a scoped invariant culture instead of using number and hex formatting wrappers. Preserve safe escaping for comments containing dynamic input. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...rateNativeApplicationConfigSourcesTests.cs | 1 + .../Tasks/LlvmIrWriterTests.cs | 35 +++- .../Utilities/EnvironmentHelper.cs | 2 +- .../Utilities/AppEnvironmentVariableTable.cs | 12 +- ...pplicationConfigNativeAssemblyGenerator.cs | 166 +++++++++--------- ...ressedAssembliesNativeAssemblyGenerator.cs | 26 +-- .../JniRemappingAssemblyGenerator.cs | 48 ++--- .../Utilities/LlvmIrWriter.cs | 41 ++--- ...veAotEnvironmentNativeAssemblyGenerator.cs | 8 +- ...NativeAotJniInitNativeAssemblyGenerator.cs | 12 +- 10 files changed, 188 insertions(+), 163 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs index 30f2bb79ae4..4f10f54f831 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs @@ -50,6 +50,7 @@ public void EmitsApplicationConfigForAllAbis (bool emitComments) Assert.That (source, Does.Contain ("@assembly_store = "), abi); Assert.That (source, Does.Contain ("@dso_cache = "), abi); Assert.That (source, Does.Contain ("!llvm.module.flags = "), abi); + Assert.That (source, Does.Contain ("i1, ; bool uses_assembly_preload"), abi); Assert.That (source, emitComments ? Does.Contain ("; Application environment variables") : Does.Not.Contain ("; Application environment variables"), abi); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs index 705ca785eb1..ac1ba811428 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -39,7 +40,7 @@ public void StringBlobDeduplicatesStringsByOffset () public void CommentsCannotInjectLinesIntoIr () { using var output = new StringWriter (); - var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: true); + using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: true); Assert.That (writer.Comment (" first\nsecond"), Does.Not.Contain ("\n")); Assert.That (writer.TrailingComment (" first\rsecond"), Does.Not.Contain ("\r")); @@ -47,6 +48,38 @@ public void CommentsCannotInjectLinesIntoIr () Assert.That (output.ToString ().Split ('\n'), Has.Length.EqualTo (2)); } + [TestCase (false)] + [TestCase (true)] + public void InlineStructureCommentsAreUnconditional (bool emitComments) + { + using var output = new StringWriter (); + using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments); + writer.Write ("i32, ; field\nptr ; final field"); + + string text = output.ToString (); + Assert.That (text, Does.Contain ("i32, ; field")); + Assert.That (text, Does.Contain ("ptr ; final field")); + } + + [Test] + public void WriterRestoresCulture () + { + CultureInfo originalCulture = CultureInfo.CurrentCulture; + try { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo ("fr-FR"); + using var output = new StringWriter (); + using (var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: false)) { + Assert.That (CultureInfo.CurrentCulture, Is.EqualTo (CultureInfo.InvariantCulture)); + writer.WriteGlobal ("number", LlvmIrWriter.GlobalConstant, "i32", 42.ToString (), 4); + } + + Assert.That (output.ToString (), Does.Contain ("i32 42, align 4")); + Assert.That (CultureInfo.CurrentCulture, Is.EqualTo (CultureInfo.GetCultureInfo ("fr-FR"))); + } finally { + CultureInfo.CurrentCulture = originalCulture; + } + } + [Test] public void NativeAotJniInitDeduplicatesHandlers () { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index ba548184277..14d3bbeb782 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -935,7 +935,7 @@ static byte ConvertFieldToByte (string fieldName, string llvmAssemblerEnvFile, s return fv; } - // These fields are always uint32_t/uint64_t and LlvmIrWriter.Number writes unsigned decimal values, + // These fields are always uint32_t/uint64_t and the IR writer emits unsigned decimal values, // so parse them directly as unsigned. static bool TryParseInteger (string value, out uint fv) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs index bd9c32791ab..06d02a03890 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AppEnvironmentVariableTable.cs @@ -47,8 +47,8 @@ public static void WriteDeclaration (LlvmIrWriter w) w.Write ($$""" %struct.AppEnvironmentVariable = type { - i32, {{w.Comment (" uint32_t name_index")}} - i32 {{w.Comment (" uint32_t value_index")}} + i32, ; uint32_t name_index + i32 ; uint32_t value_index } """); @@ -60,8 +60,8 @@ public void Write (LlvmIrWriter w, string arrayName, string contentsName, string foreach (var e in entries) { elements.Add ($$""" %struct.AppEnvironmentVariable { - i32 {{LlvmIrWriter.Number (e.NameIndex)}}, {{w.Comment ($" '{e.Name}'")}} - i32 {{LlvmIrWriter.Number (e.ValueIndex)}}{{w.Comment ($" '{e.Value}'")}} + i32 {{e.NameIndex}}, {{w.Comment ($" '{e.Name}'")}} + i32 {{e.ValueIndex}}{{w.Comment ($" '{e.Value}'")}} } """); } @@ -69,8 +69,8 @@ public void Write (LlvmIrWriter w, string arrayName, string contentsName, string w.WriteGlobal ( arrayName, LlvmIrWriter.GlobalConstant, - $"[{LlvmIrWriter.Number (elements.Count)} x %struct.AppEnvironmentVariable]", - w.ArrayValue (elements, LlvmIrWriter.IndexComment), + $"[{elements.Count} x %struct.AppEnvironmentVariable]", + w.ArrayValue (elements, i => $" {i}"), w.GetAggregateAlignment (StructureAlignment, (ulong)elements.Count * StructureSize), comment ); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs index d2bf8dcc9e4..5d679bcdc94 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs @@ -77,7 +77,7 @@ public State (AppEnvironmentVariableTable environmentVariables, AppEnvironmentVa State? state; /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. /// public bool EmitComments { get; set; } @@ -194,7 +194,7 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName { State data = EnsureState (); - var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); var strings = new LlvmIrStringPool (); ulong structAlignment = Math.Max (w.Target.PointerSize, NonPointerMemberAlignment); @@ -203,69 +203,69 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName w.Write ($$""" %struct.ApplicationConfig = type { - i1, {{w.Comment (" bool uses_assembly_preload")}} - i1, {{w.Comment (" bool marshal_methods_enabled")}} - i1, {{w.Comment (" bool ignore_split_configs")}} - i32, {{w.Comment (" uint32_t number_of_runtime_properties")}} - i32, {{w.Comment (" uint32_t package_naming_policy")}} - i32, {{w.Comment (" uint32_t environment_variable_count")}} - i32, {{w.Comment (" uint32_t system_property_count")}} - i32, {{w.Comment (" uint32_t number_of_assemblies_in_apk")}} - i32, {{w.Comment (" uint32_t bundled_assembly_name_width")}} - i32, {{w.Comment (" uint32_t number_of_dso_cache_entries")}} - i32, {{w.Comment (" uint32_t number_of_shared_libraries")}} - i32, {{w.Comment (" uint32_t android_runtime_jnienv_class_token")}} - i32, {{w.Comment (" uint32_t jnienv_initialize_method_token")}} - i32, {{w.Comment (" uint32_t jni_remapping_replacement_type_count")}} - i32, {{w.Comment (" uint32_t jni_remapping_replacement_method_index_entry_count")}} - ptr, {{w.Comment (" char* android_package_name")}} - i1 {{w.Comment (" bool have_assembly_store")}} + i1, ; bool uses_assembly_preload + i1, ; bool marshal_methods_enabled + i1, ; bool ignore_split_configs + i32, ; uint32_t number_of_runtime_properties + i32, ; uint32_t package_naming_policy + i32, ; uint32_t environment_variable_count + i32, ; uint32_t system_property_count + i32, ; uint32_t number_of_assemblies_in_apk + i32, ; uint32_t bundled_assembly_name_width + i32, ; uint32_t number_of_dso_cache_entries + i32, ; uint32_t number_of_shared_libraries + i32, ; uint32_t android_runtime_jnienv_class_token + i32, ; uint32_t jnienv_initialize_method_token + i32, ; uint32_t jni_remapping_replacement_type_count + i32, ; uint32_t jni_remapping_replacement_method_index_entry_count + ptr, ; char* android_package_name + i1 ; bool have_assembly_store } %struct.AssemblyStoreAssemblyDescriptor = type { - i32, {{w.Comment (" uint32_t data_offset")}} - i32, {{w.Comment (" uint32_t data_size")}} - i32, {{w.Comment (" uint32_t debug_data_offset")}} - i32, {{w.Comment (" uint32_t debug_data_size")}} - i32, {{w.Comment (" uint32_t config_data_offset")}} - i32 {{w.Comment (" uint32_t config_data_size")}} + i32, ; uint32_t data_offset + i32, ; uint32_t data_size + i32, ; uint32_t debug_data_offset + i32, ; uint32_t debug_data_size + i32, ; uint32_t config_data_offset + i32 ; uint32_t config_data_size } %struct.AssemblyStoreRuntimeData = type { - ptr, {{w.Comment (" uint8_t data_start")}} - i32, {{w.Comment (" uint32_t assembly_count")}} - i32, {{w.Comment (" uint32_t index_entry_count")}} - ptr {{w.Comment (" AssemblyStoreAssemblyDescriptor assemblies")}} + ptr, ; uint8_t data_start + i32, ; uint32_t assembly_count + i32, ; uint32_t index_entry_count + ptr ; AssemblyStoreAssemblyDescriptor assemblies } %struct.AssemblyStoreSingleAssemblyRuntimeData = type { - ptr, {{w.Comment (" uint8_t image_data")}} - ptr, {{w.Comment (" uint8_t debug_info_data")}} - ptr, {{w.Comment (" uint8_t config_data")}} - ptr {{w.Comment (" AssemblyStoreAssemblyDescriptor descriptor")}} + ptr, ; uint8_t image_data + ptr, ; uint8_t debug_info_data + ptr, ; uint8_t config_data + ptr ; AssemblyStoreAssemblyDescriptor descriptor } %struct.DSOCacheEntry = type { - i32, {{w.Comment (" uint32_t hash")}} - i1, {{w.Comment (" bool ignore")}} - i1, {{w.Comment (" bool is_jni_library")}} - i32, {{w.Comment (" uint32_t name_index")}} - ptr {{w.Comment (" void* handle")}} + i32, ; uint32_t hash + i1, ; bool ignore + i1, ; bool is_jni_library + i32, ; uint32_t name_index + ptr ; void* handle } %struct.XamarinAndroidBundledAssembly = type { - i32, {{w.Comment (" int32_t file_fd")}} - ptr, {{w.Comment (" char* file_name")}} - i32, {{w.Comment (" uint32_t data_offset")}} - i32, {{w.Comment (" uint32_t data_size")}} - ptr, {{w.Comment (" uint8_t data")}} - i32, {{w.Comment (" uint32_t name_length")}} - ptr {{w.Comment (" char* name")}} + i32, ; int32_t file_fd + ptr, ; char* file_name + i32, ; uint32_t data_offset + i32, ; uint32_t data_size + ptr, ; uint8_t data + i32, ; uint32_t name_length + ptr ; char* name } """); - w.WriteGlobal ("format_tag", LlvmIrWriter.GlobalConstant, "i64", LlvmIrWriter.Number (FORMAT_TAG), 8, $" 0x{FORMAT_TAG:x}"); + w.WriteGlobal ("format_tag", LlvmIrWriter.GlobalConstant, "i64", FORMAT_TAG.ToString (), 8, $" 0x{FORMAT_TAG:x}"); data.EnvironmentVariables.Write (w, "app_environment_variables", "app_environment_variable_contents", " Application environment variables array, name:value"); data.SystemProperties.Write (w, "app_system_properties", "app_system_property_contents", " System properties defined by the application"); @@ -273,23 +273,23 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName ApplicationConfig cfg = data.ApplicationConfig; w.WriteGlobal ("application_config", LlvmIrWriter.GlobalConstant, "%struct.ApplicationConfig", $$""" { - i1 {{LlvmIrWriter.Bool (cfg.uses_assembly_preload)}}, {{w.Comment (" bool uses_assembly_preload")}} - i1 {{LlvmIrWriter.Bool (cfg.marshal_methods_enabled)}}, {{w.Comment (" bool marshal_methods_enabled")}} - i1 {{LlvmIrWriter.Bool (cfg.ignore_split_configs)}}, {{w.Comment (" bool ignore_split_configs")}} - i32 {{LlvmIrWriter.Number (cfg.number_of_runtime_properties)}}, {{w.Comment (" uint32_t number_of_runtime_properties")}} - i32 {{LlvmIrWriter.Number (cfg.package_naming_policy)}}, {{w.Comment (" uint32_t package_naming_policy")}} - i32 {{LlvmIrWriter.Number (cfg.environment_variable_count)}}, {{w.Comment (" uint32_t environment_variable_count")}} - i32 {{LlvmIrWriter.Number (cfg.system_property_count)}}, {{w.Comment (" uint32_t system_property_count")}} - i32 {{LlvmIrWriter.Number (cfg.number_of_assemblies_in_apk)}}, {{w.Comment (" uint32_t number_of_assemblies_in_apk")}} - i32 {{LlvmIrWriter.Number (cfg.bundled_assembly_name_width)}}, {{w.Comment (" uint32_t bundled_assembly_name_width")}} - i32 {{LlvmIrWriter.Number (cfg.number_of_dso_cache_entries)}}, {{w.Comment (" uint32_t number_of_dso_cache_entries")}} - i32 {{LlvmIrWriter.Number (cfg.number_of_shared_libraries)}}, {{w.Comment (" uint32_t number_of_shared_libraries")}} - i32 {{LlvmIrWriter.Hex (cfg.android_runtime_jnienv_class_token)}}, {{w.Comment (" uint32_t android_runtime_jnienv_class_token")}} - i32 {{LlvmIrWriter.Hex (cfg.jnienv_initialize_method_token)}}, {{w.Comment (" uint32_t jnienv_initialize_method_token")}} - i32 {{LlvmIrWriter.Hex (cfg.jni_remapping_replacement_type_count)}}, {{w.Comment (" uint32_t jni_remapping_replacement_type_count")}} - i32 {{LlvmIrWriter.Number (cfg.jni_remapping_replacement_method_index_entry_count)}}, {{w.Comment (" uint32_t jni_remapping_replacement_method_index_entry_count")}} - ptr {{strings.GetPointer (cfg.android_package_name, "ApplicationConfig", "android_package_name")}}, {{w.Comment (" char* android_package_name")}} - i1 {{LlvmIrWriter.Bool (cfg.have_assembly_store)}}{{w.Comment (" bool have_assembly_store")}} + i1 {{(cfg.uses_assembly_preload ? "true" : "false")}}, ; bool uses_assembly_preload + i1 {{(cfg.marshal_methods_enabled ? "true" : "false")}}, ; bool marshal_methods_enabled + i1 {{(cfg.ignore_split_configs ? "true" : "false")}}, ; bool ignore_split_configs + i32 {{cfg.number_of_runtime_properties}}, ; uint32_t number_of_runtime_properties + i32 {{cfg.package_naming_policy}}, ; uint32_t package_naming_policy + i32 {{cfg.environment_variable_count}}, ; uint32_t environment_variable_count + i32 {{cfg.system_property_count}}, ; uint32_t system_property_count + i32 {{cfg.number_of_assemblies_in_apk}}, ; uint32_t number_of_assemblies_in_apk + i32 {{cfg.bundled_assembly_name_width}}, ; uint32_t bundled_assembly_name_width + i32 {{cfg.number_of_dso_cache_entries}}, ; uint32_t number_of_dso_cache_entries + i32 {{cfg.number_of_shared_libraries}}, ; uint32_t number_of_shared_libraries + i32 u0x{{cfg.android_runtime_jnienv_class_token:x8}}, ; uint32_t android_runtime_jnienv_class_token + i32 u0x{{cfg.jnienv_initialize_method_token:x8}}, ; uint32_t jnienv_initialize_method_token + i32 u0x{{cfg.jni_remapping_replacement_type_count:x8}}, ; uint32_t jni_remapping_replacement_type_count + i32 {{cfg.jni_remapping_replacement_method_index_entry_count}}, ; uint32_t jni_remapping_replacement_method_index_entry_count + ptr {{strings.GetPointer (cfg.android_package_name, "ApplicationConfig", "android_package_name")}}, ; char* android_package_name + i1 {{(cfg.have_assembly_store ? "true" : "false")}}; bool have_assembly_store } """, w.GetAggregateAlignment (structAlignment, ApplicationConfigDataSize)); @@ -304,8 +304,8 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName w.WriteGlobal ( "init_runtime_property_names", LlvmIrWriter.GlobalConstant, - $"[{LlvmIrWriter.Number (names.Count)} x ptr]", - w.ArrayValue (names, i => $" {LlvmIrWriter.Number (i)} ('{data.RuntimePropertyNames [i]}')"), + $"[{names.Count} x ptr]", + w.ArrayValue (names, i => $" {i} ('{data.RuntimePropertyNames [i]}')"), w.GetPointerArrayAlignment (names.Count), "Names of properties passed to coreclr_initialize" ); @@ -317,20 +317,20 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName w.WriteGlobal ( "init_runtime_property_values", LlvmIrWriter.GlobalWritable, - $"[{LlvmIrWriter.Number (values.Count)} x ptr]", - w.ArrayValue (values, i => $" {LlvmIrWriter.Number (i)} ('{data.RuntimePropertyValues [i]}')"), + $"[{values.Count} x ptr]", + w.ArrayValue (values, i => $" {i} ('{data.RuntimePropertyValues [i]}')"), w.GetPointerArrayAlignment (values.Count), "Values of properties passed to coreclr_initialize" ); ulong assemblyCount = (ulong)NumberOfAssembliesInApk; - w.WriteGlobal ("assembly_store_bundled_assemblies", LlvmIrWriter.GlobalWritable, $"[{LlvmIrWriter.Number (assemblyCount)} x %struct.AssemblyStoreSingleAssemblyRuntimeData]", "zeroinitializer", w.GetAggregateAlignment (w.Target.PointerSize, assemblyCount * AssemblyStoreSingleAssemblyRuntimeDataDataSize)); + w.WriteGlobal ("assembly_store_bundled_assemblies", LlvmIrWriter.GlobalWritable, $"[{assemblyCount} x %struct.AssemblyStoreSingleAssemblyRuntimeData]", "zeroinitializer", w.GetAggregateAlignment (w.Target.PointerSize, assemblyCount * AssemblyStoreSingleAssemblyRuntimeDataDataSize)); w.WriteGlobal ("assembly_store", LlvmIrWriter.GlobalWritable, "%struct.AssemblyStoreRuntimeData", $$""" { - ptr null, {{w.Comment (" uint8_t* data_start")}} - i32 0, {{w.Comment (" uint32_t assembly_count")}} - i32 0, {{w.Comment (" uint32_t index_entry_count")}} - ptr null{{w.Comment (" AssemblyStoreAssemblyDescriptor* assemblies")}} + ptr null, ; uint8_t* data_start + i32 0, ; uint32_t assembly_count + i32 0, ; uint32_t index_entry_count + ptr null; AssemblyStoreAssemblyDescriptor* assemblies } """, w.GetAggregateAlignment (structAlignment, AssemblyStoreRuntimeDataDataSize)); @@ -349,24 +349,24 @@ void WriteDsoCache (LlvmIrWriter w, State state, ulong structAlignment) foreach (DSOCacheEntry entry in state.DsoCache) { entries.Add ($$""" %struct.DSOCacheEntry { - i32 {{LlvmIrWriter.Hex (entry.hash)}}, {{w.Comment ($" from name: {entry.HashedName}")}} - i1 {{LlvmIrWriter.Bool (entry.ignore)}}, {{w.Comment (" bool ignore")}} - i1 {{LlvmIrWriter.Bool (entry.is_jni_library)}}, {{w.Comment (" bool is_jni_library")}} - i32 {{LlvmIrWriter.Number (entry.name_index)}}, {{w.Comment ($" name: {entry.RealName}")}} - ptr null{{w.Comment (" void* handle")}} + i32 u0x{{entry.hash:x8}}, {{w.Comment ($" from name: {entry.HashedName}")}} + i1 {{(entry.ignore ? "true" : "false")}}, ; bool ignore + i1 {{(entry.is_jni_library ? "true" : "false")}}, ; bool is_jni_library + i32 {{entry.name_index}}, {{w.Comment ($" name: {entry.RealName}")}} + ptr null; void* handle } """); } w.WriteGlobal ( "dso_cache", LlvmIrWriter.GlobalWritable, - $"[{LlvmIrWriter.Number (entries.Count)} x %struct.DSOCacheEntry]", - w.ArrayValue (entries, LlvmIrWriter.IndexComment), + $"[{entries.Count} x %struct.DSOCacheEntry]", + w.ArrayValue (entries, i => $" {i}"), w.GetAggregateAlignment (structAlignment, (ulong)entries.Count * DSOCacheEntryDataSize), " DSO cache entries" ); - w.WriteGlobal ("dso_jni_preloads_idx_stride", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (state.NameMutationsCount), 4); + w.WriteGlobal ("dso_jni_preloads_idx_stride", LlvmIrWriter.GlobalConstant, "i32", state.NameMutationsCount.ToString (), 4); // Indices array MUST NOT be sorted, since it groups alias entries together with the main entry var indices = new List (state.JniPreloadDSOs.Count); @@ -377,16 +377,16 @@ void WriteDsoCache (LlvmIrWriter w, State state, ulong structAlignment) throw new InvalidOperationException ($"Internal error: DSO entry in JNI preload list not found in the DSO cache list."); } - indices.Add ($"\ti32 {LlvmIrWriter.Number (dsoIdx)}"); + indices.Add ($"\ti32 {dsoIdx}"); indexNames.Add (preload.HashedName); } // Historically, the count has always been a 64-bit integer - w.WriteGlobal ("dso_jni_preloads_idx_count", LlvmIrWriter.GlobalConstant, "i64", LlvmIrWriter.Number (indices.Count), 8); + w.WriteGlobal ("dso_jni_preloads_idx_count", LlvmIrWriter.GlobalConstant, "i64", indices.Count.ToString (), 8); w.WriteGlobal ( "dso_jni_preloads_idx", LlvmIrWriter.GlobalConstant, - $"[{LlvmIrWriter.Number (indices.Count)} x i32]", + $"[{indices.Count} x i32]", w.ArrayValue (indices, i => $" {indexNames [i]}"), w.GetAggregateAlignment (4, (ulong)indices.Count * 4), " Indices into dso_cache[] of DSO libraries to preload because of JNI use" diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs index d209f3d1245..65fb870ad0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs @@ -36,7 +36,7 @@ sealed class CompressedAssemblyDescriptor readonly Dictionary archBufferSizes = new (); /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. /// public bool EmitComments { get; set; } @@ -80,14 +80,14 @@ public CompressedAssembliesNativeAssemblyGenerator (TaskLoggingHelper log, IDict public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); w.WriteHeader (fileName); w.Write ($$""" %struct.CompressedAssemblyDescriptor = type { - i32, {{w.Comment (" uint32_t uncompressed_file_size")}} - i1, {{w.Comment (" bool loaded")}} - i32 {{w.Comment (" uint32_t buffer_offset")}} + i32, ; uint32_t uncompressed_file_size + i1, ; bool loaded + i32 ; uint32_t buffer_offset } """); @@ -113,7 +113,7 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName static void WriteCount (LlvmIrWriter w, uint count) { - w.WriteGlobal (CompressedAssemblyCountSymbolName, LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (count), 4); + w.WriteGlobal (CompressedAssemblyCountSymbolName, LlvmIrWriter.GlobalConstant, "i32", count.ToString (), 4); } static void WriteDescriptors (LlvmIrWriter w, List descriptors) @@ -122,9 +122,9 @@ static void WriteDescriptors (LlvmIrWriter w, List foreach (CompressedAssemblyDescriptor d in descriptors) { elements.Add ($$""" %struct.CompressedAssemblyDescriptor { - i32 {{LlvmIrWriter.Number (d.uncompressed_file_size)}}, {{w.Comment (" uint32_t uncompressed_file_size")}} - i1 false, {{w.Comment (" bool loaded")}} - i32 {{LlvmIrWriter.Number (d.buffer_offset)}}{{w.Comment (" uint32_t buffer_offset")}} + i32 {{d.uncompressed_file_size}}, ; uint32_t uncompressed_file_size + i1 false, ; bool loaded + i32 {{d.buffer_offset}}; uint32_t buffer_offset } """); } @@ -132,16 +132,16 @@ static void WriteDescriptors (LlvmIrWriter w, List w.WriteGlobal ( DescriptorsArraySymbolName, LlvmIrWriter.GlobalWritable, - $"[{LlvmIrWriter.Number (descriptors.Count)} x %struct.CompressedAssemblyDescriptor]", - w.ArrayValue (elements, i => $" {LlvmIrWriter.Number (i)}: {descriptors [i].AssemblyName}"), + $"[{descriptors.Count} x %struct.CompressedAssemblyDescriptor]", + w.ArrayValue (elements, i => $" {i}: {descriptors [i].AssemblyName}"), w.GetAggregateAlignment (DescriptorAlignment, (ulong)descriptors.Count * DescriptorDataSize) ); } static void WriteBuffer (LlvmIrWriter w, uint bufferSize) { - w.WriteGlobal (UncompressedAssembliesBufferSizeSymbolName, LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (bufferSize), 4); - w.WriteGlobal (UncompressedAssembliesBufferSymbolName, LlvmIrWriter.GlobalWritable, $"[{LlvmIrWriter.Number (bufferSize)} x i8]", "zeroinitializer", w.GetAggregateAlignment (1, bufferSize)); + w.WriteGlobal (UncompressedAssembliesBufferSizeSymbolName, LlvmIrWriter.GlobalConstant, "i32", bufferSize.ToString (), 4); + w.WriteGlobal (UncompressedAssembliesBufferSymbolName, LlvmIrWriter.GlobalWritable, $"[{bufferSize} x i8]", "zeroinitializer", w.GetAggregateAlignment (1, bufferSize)); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 90817da13a1..12f231e779f 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -122,7 +122,7 @@ public JniRemappingTypeReplacementEntry (JniRemappingTypeReplacement mtr) public int ReplacementMethodIndexEntryCount { get; private set; } = 0; /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. /// public bool EmitComments { get; set; } @@ -173,7 +173,7 @@ public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List $" {i}"), w.GetAggregateAlignment (alignment, (ulong)elements.Count * dataSize) ); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs index 62883cffa6d..539ab7eb625 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrWriter.cs @@ -15,7 +15,7 @@ namespace Xamarin.Android.Tasks; /// takes care of the parts shared by all of them: the module header and metadata footer, optional /// comments, string literals and alignment of global symbols. /// -sealed class LlvmIrWriter +sealed class LlvmIrWriter : IDisposable { // Symbol attributes, see https://llvm.org/docs/LangRef.html#global-variables public const string GlobalConstant = "dso_local local_unnamed_addr constant"; @@ -24,13 +24,13 @@ sealed class LlvmIrWriter public const string LocalString = "private unnamed_addr constant"; readonly TextWriter output; + readonly CultureInfo previousCulture; public LlvmIrTarget Target { get; } /// - /// Whether descriptive comments are written to the generated LLVM IR. Comments make the - /// output easier to read, but they can account for the majority of the file's size and - /// have no effect on the code llc produces. + /// Whether additional descriptive comments are written to the generated LLVM IR. + /// Inline structure comments are always written. /// public bool EmitComments { get; } @@ -39,8 +39,12 @@ public LlvmIrWriter (TextWriter output, LlvmIrTarget target, bool emitComments) this.output = output ?? throw new ArgumentNullException (nameof (output)); Target = target ?? throw new ArgumentNullException (nameof (target)); EmitComments = emitComments; + previousCulture = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; } + public void Dispose () => CultureInfo.CurrentCulture = previousCulture; + /// /// Writes verbatim, except that line breaks (which may come from the /// C# source files the raw string literals live in) are written using the output's newline @@ -76,8 +80,8 @@ public void WriteLine (string text = "") } /// - /// Returns as an LLVM IR comment (i.e. prefixed with ;) if comments are - /// enabled, an empty string otherwise. The text is expected to contain any leading whitespace. + /// Renders dynamic comment text (which may contain line breaks) safely. + /// Static comments can be written directly in IR literals. /// public string Comment (string? text) => EmitComments && !text.IsNullOrEmpty () ? $";{SanitizeComment (text)}" : ""; @@ -126,7 +130,7 @@ public void WriteGlobal (string name, string attributes, string type, string val { WriteLine (); WriteCommentLine (comment); - WriteLine ($"@{name} = {attributes} {type} {value}, align {Number (alignment)}"); + WriteLine ($"@{name} = {attributes} {type} {value}, align {alignment}"); } /// @@ -161,11 +165,6 @@ void AppendCommentOrNewline (int index) } } - /// - /// Returns the conventional array element comment, i.e. the element index. - /// - public static string IndexComment (int index) => $" {Number (index)}"; - /// /// Alignment of a structure or an array with elements of the given size and alignment. /// @@ -183,8 +182,8 @@ public void WriteMetadata () var targetFlags = new StringBuilder (); for (int i = 0; i < Target.ModuleFlags.Length; i++) { int n = i + 7; - flags.Append ($", !{Number (n)}"); - targetFlags.Append ($"!{Number (n)} = !{{{Target.ModuleFlags [i]}}}\n"); + flags.Append ($", !{n}"); + targetFlags.Append ($"!{n} = !{{{Target.ModuleFlags [i]}}}\n"); } WriteLine (); @@ -225,14 +224,6 @@ public void WriteExternalFunctionDeclarations (ICollection names) } } - public static string Number (int value) => value.ToString (CultureInfo.InvariantCulture); - public static string Number (uint value) => value.ToString (CultureInfo.InvariantCulture); - public static string Number (ulong value) => value.ToString (CultureInfo.InvariantCulture); - - public static string Hex (uint value) => $"u0x{value.ToString ("x8", CultureInfo.InvariantCulture)}"; - - public static string Bool (bool value) => value ? "true" : "false"; - /// /// Renders as an LLVM IR string constant (c"..."), escaping all the /// characters which aren't printable ASCII as well as " and \. @@ -290,7 +281,7 @@ public int Add (string s) return offset; } - public string Type => $"[{LlvmIrWriter.Number (Size)} x i8]"; + public string Type => $"[{Size} x i8]"; public string Value () { @@ -361,7 +352,7 @@ public string GetSymbol (string value, string? groupName = null, string? symbolS group = AddGroup (groupName, groupName); } - string symbol = $"{group.SymbolPrefix}.{LlvmIrWriter.Number (group.Strings.Count)}"; + string symbol = $"{group.SymbolPrefix}.{group.Strings.Count}"; if (!symbolSuffix.IsNullOrEmpty ()) { symbol = $"{symbol}_{symbolSuffix}"; } @@ -407,7 +398,7 @@ public void Write (LlvmIrWriter writer) foreach ((string symbol, string value) in group.Strings) { byte[] bytes = MonoAndroidHelper.Utf8StringToBytes (value); ulong size = (ulong)bytes.Length + 1; - writer.WriteLine ($"@{symbol} = {LlvmIrWriter.LocalString} [{LlvmIrWriter.Number (size)} x i8] {LlvmIrWriter.QuoteBytes (bytes, nullTerminated: true)}, align {LlvmIrWriter.Number (writer.GetAggregateAlignment (1, size))}"); + writer.WriteLine ($"@{symbol} = {LlvmIrWriter.LocalString} [{size} x i8] {LlvmIrWriter.QuoteBytes (bytes, nullTerminated: true)}, align {writer.GetAggregateAlignment (1, size)}"); } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs index 49f3aaf4f6a..ed48a6ae106 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs @@ -14,7 +14,7 @@ class NativeAotEnvironmentNativeAssemblyGenerator readonly AppEnvironmentVariableTable systemProperties; /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. /// public bool EmitComments { get; set; } @@ -30,14 +30,14 @@ public NativeAotEnvironmentNativeAssemblyGenerator (TaskLoggingHelper log, Envir public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); w.WriteHeader (fileName); AppEnvironmentVariableTable.WriteDeclaration (w); - w.WriteGlobal ("__naot_android_app_environment_variable_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (environmentVariables.Count), 4); + w.WriteGlobal ("__naot_android_app_environment_variable_count", LlvmIrWriter.GlobalConstant, "i32", environmentVariables.Count.ToString (), 4); environmentVariables.Write (w, "__naot_android_app_environment_variables", "__naot_android_app_environment_variable_contents", " Application environment variables array, name:value"); - w.WriteGlobal ("__naot_android_app_system_property_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (systemProperties.Count), 4); + w.WriteGlobal ("__naot_android_app_system_property_count", LlvmIrWriter.GlobalConstant, "i32", systemProperties.Count.ToString (), 4); // We reuse the same structure as for environment variables, there's no point in adding a new, identical, one systemProperties.Write (w, "__naot_android_app_system_properties", "__naot_android_app_system_property_contents", " System properties defined by the application"); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs index d222081c578..1d09dcbc09a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs @@ -14,7 +14,7 @@ class NativeAotJniInitNativeAssemblyGenerator readonly List jniOnLoadNames = new (); /// - /// Whether to write descriptive comments into the generated LLVM IR. Defaults to false. + /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. /// public bool EmitComments { get; set; } @@ -46,11 +46,11 @@ void CollectHandlers (List? handlers) public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); var strings = new LlvmIrStringPool (); w.WriteHeader (fileName); - w.WriteGlobal ("__jni_on_load_handler_count", LlvmIrWriter.GlobalConstant, "i32", LlvmIrWriter.Number (jniOnLoadNames.Count), 4); + w.WriteGlobal ("__jni_on_load_handler_count", LlvmIrWriter.GlobalConstant, "i32", jniOnLoadNames.Count.ToString (), 4); var handlers = new List (jniOnLoadNames.Count); var names = new List (jniOnLoadNames.Count); @@ -59,10 +59,10 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName names.Add ($"\tptr {strings.GetPointer (name)}"); } - string type = $"[{LlvmIrWriter.Number (handlers.Count)} x ptr]"; + string type = $"[{handlers.Count} x ptr]"; ulong alignment = w.GetPointerArrayAlignment (handlers.Count); - w.WriteGlobal ("__jni_on_load_handlers", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (handlers, LlvmIrWriter.IndexComment), alignment); - w.WriteGlobal ("__jni_on_load_handler_names", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (names, i => $" {LlvmIrWriter.Number (i)} ('{jniOnLoadNames [i]}')"), alignment); + w.WriteGlobal ("__jni_on_load_handlers", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (handlers, i => $" {i}"), alignment); + w.WriteGlobal ("__jni_on_load_handler_names", LlvmIrWriter.GlobalConstant, type, w.ArrayValue (names, i => $" {i} ('{jniOnLoadNames [i]}')"), alignment); strings.Write (w); From b7043e91c07a19cd0a972f5975a8b8b98b294685 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 13:22:29 +0200 Subject: [PATCH 39/58] Always emit LLVM IR comments Remove the comment toggle from LLVM IR generators, tasks, tests, and MSBuild wiring so comments are written directly without configuration. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...teCompressedAssembliesNativeSourceFiles.cs | 11 +-------- .../GenerateNativeApplicationConfigSources.cs | 8 ------- ...rateNativeApplicationConfigSourcesTests.cs | 10 ++++---- .../Tasks/LlvmIrWriterTests.cs | 11 ++++----- ...pplicationConfigNativeAssemblyGenerator.cs | 8 +------ ...ressedAssembliesNativeAssemblyGenerator.cs | 7 +----- .../JniRemappingAssemblyGenerator.cs | 7 +----- .../Utilities/LlvmIrWriter.cs | 23 +++++++------------ ...veAotEnvironmentNativeAssemblyGenerator.cs | 7 +----- ...NativeAotJniInitNativeAssemblyGenerator.cs | 7 +----- .../Xamarin.Android.Common.targets | 2 -- 11 files changed, 23 insertions(+), 78 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs index a25becf0fad..4423711689b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateCompressedAssembliesNativeSourceFiles.cs @@ -31,13 +31,6 @@ public class GenerateCompressedAssembliesNativeSourceFiles : AndroidTask [Required] public string ProjectFullPath { get; set; } = ""; - /// - /// When true, descriptive comments are written into the generated LLVM IR. They make - /// the .ll far easier to read, but have no effect on the object code produced from it. - /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. - /// - public bool EmitLlvmIrComments { get; set; } - public override bool RunTask () { GenerateCompressedAssemblySources (); @@ -100,9 +93,7 @@ void GenerateCompressedAssemblySources () void Generate (Dictionary>? dict) { - var generator = new CompressedAssembliesNativeAssemblyGenerator (Log, dict) { - EmitComments = EmitLlvmIrComments, - }; + var generator = new CompressedAssembliesNativeAssemblyGenerator (Log, dict); foreach (string abi in SupportedAbis) { string baseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"compressed_assemblies.{abi.ToLowerInvariant ()}"); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 159660a70a6..83a9bde0f63 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -52,13 +52,6 @@ public class GenerateNativeApplicationConfigSources : AndroidTask [Required] public string AndroidRuntime { get; set; } = ""; - /// - /// When true, descriptive comments are written into the generated LLVM IR. They make - /// the .ll far easier to read, but have no effect on the object code produced from it. - /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. - /// - public bool EmitLlvmIrComments { get; set; } - public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty; public string? ProjectRuntimeConfigDevFilePath { get; set; } @@ -243,7 +236,6 @@ static bool ShouldSkipAssembly (ITaskItem assembly) MarshalMethodsEnabled = false, IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (), HaveAssemblyStore = UseAssemblyStore, - EmitComments = EmitLlvmIrComments, }; // Any errors in the input data must be reported before any of the output files is written diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs index 4f10f54f831..a2fa4fef16d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeApplicationConfigSourcesTests.cs @@ -11,11 +11,10 @@ namespace Xamarin.Android.Build.Tests.Tasks; [TestFixture] public class GenerateNativeApplicationConfigSourcesTests : BaseTest { - [TestCase (false)] - [TestCase (true)] - public void EmitsApplicationConfigForAllAbis (bool emitComments) + [Test] + public void EmitsApplicationConfigForAllAbis () { - string outputRoot = Path.Combine (Root, "temp", $"{nameof (EmitsApplicationConfigForAllAbis)}-{emitComments}"); + string outputRoot = Path.Combine (Root, "temp", nameof (EmitsApplicationConfigForAllAbis)); string monoAndroidPath = Path.Combine (TestEnvironment.MonoAndroidFrameworkDirectory, "Mono.Android.dll"); FileAssert.Exists (monoAndroidPath); @@ -28,7 +27,6 @@ public void EmitsApplicationConfigForAllAbis (bool emitComments) EnablePreloadAssembliesDefault = false, AndroidRuntime = "CoreCLR", UseAssemblyStore = true, - EmitLlvmIrComments = emitComments, }; Assert.IsTrue (task.Execute (), "GenerateNativeApplicationConfigSources should succeed."); @@ -51,7 +49,7 @@ public void EmitsApplicationConfigForAllAbis (bool emitComments) Assert.That (source, Does.Contain ("@dso_cache = "), abi); Assert.That (source, Does.Contain ("!llvm.module.flags = "), abi); Assert.That (source, Does.Contain ("i1, ; bool uses_assembly_preload"), abi); - Assert.That (source, emitComments ? Does.Contain ("; Application environment variables") : Does.Not.Contain ("; Application environment variables"), abi); + Assert.That (source, Does.Contain ("; Application environment variables"), abi); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs index ac1ba811428..3c4c391fa52 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrWriterTests.cs @@ -40,7 +40,7 @@ public void StringBlobDeduplicatesStringsByOffset () public void CommentsCannotInjectLinesIntoIr () { using var output = new StringWriter (); - using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: true); + using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64); Assert.That (writer.Comment (" first\nsecond"), Does.Not.Contain ("\n")); Assert.That (writer.TrailingComment (" first\rsecond"), Does.Not.Contain ("\r")); @@ -48,12 +48,11 @@ public void CommentsCannotInjectLinesIntoIr () Assert.That (output.ToString ().Split ('\n'), Has.Length.EqualTo (2)); } - [TestCase (false)] - [TestCase (true)] - public void InlineStructureCommentsAreUnconditional (bool emitComments) + [Test] + public void WritesInlineStructureComments () { using var output = new StringWriter (); - using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments); + using var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64); writer.Write ("i32, ; field\nptr ; final field"); string text = output.ToString (); @@ -68,7 +67,7 @@ public void WriterRestoresCulture () try { CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo ("fr-FR"); using var output = new StringWriter (); - using (var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64, emitComments: false)) { + using (var writer = new LlvmIrWriter (output, LlvmIrTarget.Arm64)) { Assert.That (CultureInfo.CurrentCulture, Is.EqualTo (CultureInfo.InvariantCulture)); writer.WriteGlobal ("number", LlvmIrWriter.GlobalConstant, "i32", 42.ToString (), 4); } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs index 5d679bcdc94..f989687aea3 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs @@ -76,12 +76,6 @@ public State (AppEnvironmentVariableTable environmentVariables, AppEnvironmentVa readonly SortedDictionary runtimeProperties; State? state; - /// - /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. - /// Set from the $(_AndroidEmitLlvmIrComments) MSBuild property. - /// - public bool EmitComments { get; set; } - public bool UsesAssemblyPreload { get; set; } public string AndroidPackageName { get; set; } = ""; public int NumberOfAssembliesInApk { get; set; } @@ -194,7 +188,7 @@ public void Generate (AndroidTargetArch arch, TextWriter output, string fileName { State data = EnsureState (); - using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch)); var strings = new LlvmIrStringPool (); ulong structAlignment = Math.Max (w.Target.PointerSize, NonPointerMemberAlignment); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs index 65fb870ad0c..cc713b9f6d1 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/CompressedAssembliesNativeAssemblyGenerator.cs @@ -35,11 +35,6 @@ sealed class CompressedAssemblyDescriptor readonly Dictionary> archDescriptors = new (); readonly Dictionary archBufferSizes = new (); - /// - /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. - /// - public bool EmitComments { get; set; } - public CompressedAssembliesNativeAssemblyGenerator (TaskLoggingHelper log, IDictionary>? archAssemblies) { if (log == null) { @@ -80,7 +75,7 @@ public CompressedAssembliesNativeAssemblyGenerator (TaskLoggingHelper log, IDict public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch)); w.WriteHeader (fileName); w.Write ($$""" diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index 12f231e779f..a7682cfae1c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -121,11 +121,6 @@ public JniRemappingTypeReplacementEntry (JniRemappingTypeReplacement mtr) public int ReplacementMethodIndexEntryCount { get; private set; } = 0; - /// - /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. - /// - public bool EmitComments { get; set; } - public JniRemappingAssemblyGenerator (TaskLoggingHelper log) { if (log == null) { @@ -173,7 +168,7 @@ public JniRemappingAssemblyGenerator (TaskLoggingHelper log, List /// Minimal helper used by the native assembly generators to write textual LLVM IR. The generators /// write the IR text themselves (mostly as interpolated raw string literals), this class merely -/// takes care of the parts shared by all of them: the module header and metadata footer, optional +/// takes care of the parts shared by all of them: the module header and metadata footer, /// comments, string literals and alignment of global symbols. /// sealed class LlvmIrWriter : IDisposable @@ -28,17 +28,10 @@ sealed class LlvmIrWriter : IDisposable public LlvmIrTarget Target { get; } - /// - /// Whether additional descriptive comments are written to the generated LLVM IR. - /// Inline structure comments are always written. - /// - public bool EmitComments { get; } - - public LlvmIrWriter (TextWriter output, LlvmIrTarget target, bool emitComments) + public LlvmIrWriter (TextWriter output, LlvmIrTarget target) { this.output = output ?? throw new ArgumentNullException (nameof (output)); Target = target ?? throw new ArgumentNullException (nameof (target)); - EmitComments = emitComments; previousCulture = CultureInfo.CurrentCulture; CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; } @@ -83,16 +76,16 @@ public void WriteLine (string text = "") /// Renders dynamic comment text (which may contain line breaks) safely. /// Static comments can be written directly in IR literals. /// - public string Comment (string? text) => EmitComments && !text.IsNullOrEmpty () ? $";{SanitizeComment (text)}" : ""; + public string Comment (string? text) => !text.IsNullOrEmpty () ? $";{SanitizeComment (text)}" : ""; /// - /// Returns a comment which follows a value on the same line (separated with a space), if comments are enabled. + /// Returns a comment which follows a value on the same line (separated with a space). /// - public string TrailingComment (string? text) => EmitComments && !text.IsNullOrEmpty () ? $" ;{SanitizeComment (text)}" : ""; + public string TrailingComment (string? text) => !text.IsNullOrEmpty () ? $" ;{SanitizeComment (text)}" : ""; public void WriteCommentLine (string? text) { - if (!EmitComments || text.IsNullOrEmpty ()) { + if (text.IsNullOrEmpty ()) { return; } @@ -136,7 +129,7 @@ public void WriteGlobal (string name, string attributes, string type, string val /// /// Renders an array initializer in which every element is written on its own line. Elements must /// already be indented. may return a comment which is - /// placed after the element when comments are enabled. + /// placed after the element. /// public string ArrayValue (IList elements, Func? getElementComment = null) { @@ -217,7 +210,7 @@ public void WriteExternalFunctionDeclarations (ICollection names) sorted.Sort ((string a, string b) => a.CompareTo (b)); WriteLine (); - Write (Comment (" External functions")); + Write ("; External functions"); foreach (string name in sorted) { WriteLine (); Write ($"declare void @{name}() local_unnamed_addr"); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs index ed48a6ae106..409d002a1d9 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotEnvironmentNativeAssemblyGenerator.cs @@ -13,11 +13,6 @@ class NativeAotEnvironmentNativeAssemblyGenerator readonly AppEnvironmentVariableTable environmentVariables; readonly AppEnvironmentVariableTable systemProperties; - /// - /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. - /// - public bool EmitComments { get; set; } - public NativeAotEnvironmentNativeAssemblyGenerator (TaskLoggingHelper log, EnvironmentBuilder envBuilder) { if (log == null) { @@ -30,7 +25,7 @@ public NativeAotEnvironmentNativeAssemblyGenerator (TaskLoggingHelper log, Envir public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch)); w.WriteHeader (fileName); AppEnvironmentVariableTable.WriteDeclaration (w); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs index 1d09dcbc09a..9eb4f77d49a 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs @@ -13,11 +13,6 @@ class NativeAotJniInitNativeAssemblyGenerator { readonly List jniOnLoadNames = new (); - /// - /// Whether to write additional descriptive comments into the generated LLVM IR. Defaults to false. - /// - public bool EmitComments { get; set; } - public NativeAotJniInitNativeAssemblyGenerator (TaskLoggingHelper log, List? runtimeComponentsJniOnLoadHandlers, List? customJniOnLoadHandlers) { if (log == null) { @@ -46,7 +41,7 @@ void CollectHandlers (List? handlers) public void Generate (AndroidTargetArch arch, TextWriter output, string fileName) { - using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch), EmitComments); + using var w = new LlvmIrWriter (output, LlvmIrTarget.Get (arch)); var strings = new LlvmIrStringPool (); w.WriteHeader (fileName); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index aef387cbfcc..ead7adf16bd 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1649,7 +1649,6 @@ because xbuild doesn't support framework reference assemblies. EnvironmentOutputDirectory="$(IntermediateOutputPath)android" Environments="@(_EnvironmentFiles)" Debug="$(AndroidIncludeDebugSymbols)" - EmitLlvmIrComments="$(_AndroidEmitLlvmIrComments)" AndroidSequencePointsMode="$(_SequencePointsMode)" EnableSGenConcurrent="$(AndroidEnableSGenConcurrent)" SupportedAbis="@(_BuildTargetAbis)" @@ -1898,7 +1897,6 @@ because xbuild doesn't support framework reference assemblies. EnvironmentOutputDirectory="$(IntermediateOutputPath)android" SupportedAbis="@(_BuildTargetAbis)" Debug="$(AndroidIncludeDebugSymbols)" - EmitLlvmIrComments="$(_AndroidEmitLlvmIrComments)" EnableCompression="$(AndroidEnableAssemblyCompression)" ProjectFullPath="$(MSBuildProjectFullPath)" /> From 40b810b8c4ceede458c27a3b3173e05784ac51b1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 08:21:51 +0200 Subject: [PATCH 40/58] Remove legacy LLVM IR type map build pipeline Keep native host compatibility stubs and P/Invoke preservation while removing unreachable typemap and marshal-method generation, rewriting, and linker steps. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../building-apps/build-properties.md | 12 +- Documentation/docs-mobile/messages/index.md | 2 +- Documentation/docs-mobile/messages/xa1049.md | 20 +- .../MonoDroid.Tuner/FindJavaObjectsStep.cs | 14 +- .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 74 -- .../Xamarin.Android.Resource.Designer.targets | 6 +- ...oft.Android.Sdk.AssemblyResolution.targets | 1 - .../Microsoft.Android.Sdk.BuildOrder.targets | 1 - ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 454 ------- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 12 +- .../Properties/Resources.Designer.cs | 2 +- .../Properties/Resources.resx | 4 +- .../Tasks/AssemblyModifierPipeline.cs | 12 - .../Tasks/CollectTypeMapFilesForArchive.cs | 38 - .../Tasks/CreateTypeManagerJava.cs | 80 -- .../Tasks/GenerateACWMap.cs | 57 - .../GenerateAdditionalProviderSources.cs | 100 +- .../Tasks/GenerateEmptyTypemapStub.cs | 10 - .../Tasks/GenerateJavaCallableWrappers.cs | 109 -- .../Tasks/GenerateJavaStubs.cs | 242 ---- .../Tasks/GenerateMainAndroidManifest.cs | 142 --- .../GenerateNativeAotBootstrapSources.cs | 6 +- ...ateNativeAotLibraryLoadAssemblerSources.cs | 2 +- .../GenerateNativeApplicationConfigSources.cs | 9 +- .../GenerateNativeMarshalMethodSources.cs | 191 +-- .../Tasks/GenerateTypeMappings.cs | 177 --- .../Tasks/GetMonoPlatformJar.cs | 43 - .../Tasks/PreTrimmingFixLegacyDesigner.cs | 94 -- .../Tasks/PrepareAbiItems.cs | 5 +- .../Tasks/RemoveRegisterAttribute.cs | 78 -- .../Tasks/RewriteMarshalMethods.cs | 122 -- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 73 +- .../IncrementalBuildTest.cs | 1 - .../ManifestTest.cs | 2 +- .../MarshalMethodTests.cs | 58 - .../Tasks/LinkerTests.cs | 18 - .../Tasks/LlvmIrGeneratorTests.cs | 33 +- .../Utilities/AssemblyPipeline.cs | 1 - .../Utilities/JCWGenerator.cs | 349 ------ .../Utilities/ManifestDocument.cs | 2 +- .../Utilities/MarshalMethodCecilAdapter.cs | 261 ---- .../MarshalMethodsAssemblyRewriter.cs | 562 --------- .../Utilities/MarshalMethodsClassifier.cs | 1107 ----------------- .../Utilities/MarshalMethodsCollection.cs | 418 ------- .../Utilities/MarshalMethodsHelpers.cs | 61 - .../MarshalMethodsNativeAssemblyGenerator.cs | 80 +- ...rshalMethodsNativeAssemblyGeneratorBase.cs | 919 -------------- .../Utilities/MarshalMethodsState.cs | 43 - .../Utilities/MonoAndroidHelper.cs | 20 +- .../Utilities/NativeCodeGenState.cs | 56 - .../Utilities/NativeTypeMappingData.cs | 67 - ...PreservePinvokesNativeAssemblyGenerator.cs | 20 +- .../Utilities/TypeMapCecilAdapter.cs | 272 ---- .../Utilities/TypeMapGenerator.cs | 355 ------ .../Utilities/TypeMapObjectsXmlFile.cs | 366 ------ .../Utilities/TypeMappingAssemblyGenerator.cs | 16 - ...TypeMappingDebugNativeAssemblyGenerator.cs | 281 ----- ...peMappingReleaseNativeAssemblyGenerator.cs | 486 -------- .../Xamarin.Android.Build.Tasks.csproj | 4 - .../Xamarin.Android.Common.targets | 118 +- .../Tests/InstallAndRunTests.cs | 15 +- .../Tests/MarshalMethodsGCHangTests.cs | 131 -- 62 files changed, 163 insertions(+), 8151 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CollectTypeMapFilesForArchive.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CreateTypeManagerJava.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaCallableWrappers.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateMainAndroidManifest.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GetMonoPlatformJar.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/PreTrimmingFixLegacyDesigner.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/RemoveRegisterAttribute.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/MarshalMethodTests.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/JCWGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsCollection.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsHelpers.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorBase.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsState.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NativeCodeGenState.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/NativeTypeMappingData.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingAssemblyGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingReleaseNativeAssemblyGenerator.cs delete mode 100644 tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs diff --git a/Documentation/docs-mobile/building-apps/build-properties.md b/Documentation/docs-mobile/building-apps/build-properties.md index 9ce8698e830..98440a023b3 100644 --- a/Documentation/docs-mobile/building-apps/build-properties.md +++ b/Documentation/docs-mobile/building-apps/build-properties.md @@ -445,15 +445,9 @@ The default value is `False` when ## AndroidEnableMarshalMethods -A bool property, that determines whether or not LLVM marshal methods are enabled. -LLVM marshal methods are an app startup optimization which uses native entry points -for Java `native` method registration. - -This property defaults to `False`. - -This property cannot be set to `true` when -[`$(PublishReadyToRun)`](#publishreadytorun) is `true`. Setting both properties -to `true` stops the build with [XA1049](../messages/xa1049.md). +A legacy boolean property for LLVM marshal methods. Marshal methods are no +longer supported. Remove this property or set it to `false`; setting it to +`true` produces error XA1049. Added in .NET 8. diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 27095488e87..36516f51b16 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -159,7 +159,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA1046](xa1046.md): Attribute '{0}' in element '{1}' has value '{2}' that cannot be parsed as boolean; {3} line {4}. + [XA1047](xa1047.md): Required attribute '{0}' missing from element '{1}'; {2} line {3}. + [XA1048](xa1048.md): '{0}' does not contain an <instrumentation> element. -+ [XA1049](xa1049.md): The 'AndroidEnableMarshalMethods' and 'PublishReadyToRun' MSBuild properties cannot both be set to 'true'. ++ [XA1049](xa1049.md): The 'AndroidEnableMarshalMethods' MSBuild property is no longer supported. + [XA1050](xa1050.md): The 'AndroidR8ObfuscationMode' MSBuild property has an invalid value. ## XA2xxx: Linker diff --git a/Documentation/docs-mobile/messages/xa1049.md b/Documentation/docs-mobile/messages/xa1049.md index 4d617f1abda..4f690ea9c43 100644 --- a/Documentation/docs-mobile/messages/xa1049.md +++ b/Documentation/docs-mobile/messages/xa1049.md @@ -11,31 +11,21 @@ f1_keywords: ## Example messages ```dotnetcli -error XA1049: The 'AndroidEnableMarshalMethods' and 'PublishReadyToRun' MSBuild properties cannot both be set to 'true'. Set one property to 'false'. +error XA1049: The 'AndroidEnableMarshalMethods' MSBuild property is no longer supported. Remove it or set it to 'false'. ``` ## Issue -[`$(AndroidEnableMarshalMethods)`](../building-apps/build-properties.md#androidenablemarshalmethods) -rewrites managed assemblies after trimming, while -[`$(PublishReadyToRun)`](../building-apps/build-properties.md#publishreadytorun) -adds native code to those assemblies. The marshal methods rewriter cannot modify -ReadyToRun assemblies. +The llvm-ir type map and its marshal-method assembly rewriter are no longer +supported. Setting `$(AndroidEnableMarshalMethods)` to `true` cannot enable +marshal methods with the trimmable type map. ## Solution -Disable marshal methods or ReadyToRun in the project file: +Remove the property or set it to `false` in the project file: ```xml false ``` - -Alternatively: - -```xml - - false - -``` diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs index d3e79f011c4..269e8f152b4 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs @@ -22,8 +22,6 @@ public class FindJavaObjectsStep : BaseStep, IAssemblyModifierPipelineStep public bool ErrorOnCustomJavaObject { get; set; } - public bool UseMarshalMethods { get; set; } - public TaskLoggingHelper Log { get; set; } public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log; @@ -74,11 +72,8 @@ bool ShouldScan (StepContext context) if (!context.IsAndroidAssembly) return false; - // When marshal methods or non-JavaPeerStyle.XAJavaInterop1 are in use we do not want to skip non-user assemblies (such as Mono.Android) - we need to generate JCWs for them during - // application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android - // build and stored in a jar file. - var useMarshalMethods = !context.IsDebug && context.EnableMarshalMethods; - var shouldSkipNonUserAssemblies = !useMarshalMethods && context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1; + // XAJavaInterop1 uses the framework JCWs from the runtime jar. + var shouldSkipNonUserAssemblies = context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1; if (shouldSkipNonUserAssemblies && !context.IsUserAssembly) { Log.LogDebugMessage ($"Skipping assembly '{context.Source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies"); @@ -114,11 +109,6 @@ List ConvertToCallableWrappers (List types) DefaultMonoRuntimeInitialization = "mono.MonoPackageManager.LoadApplication (context);", }; - if (UseMarshalMethods) { - var classifier = new MarshalMethodsClassifier (Context, Context.Resolver, Log); - reader_options.MethodClassifier = new MarshalMethodsCollection (classifier); - } - foreach (var type in types) { var wrapper = CecilImporter.CreateType (type, Context, reader_options); wrappers.Add (wrapper); diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs deleted file mode 100644 index 43a9b8de334..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Utilities; -using Mono.Cecil; -using Mono.Linker.Steps; -using Xamarin.Android.Tasks; - -namespace MonoDroid.Tuner; - -/// -/// Scans an assembly for JLOs that need to be in the typemap and writes them to an XML file. -/// -public class FindTypeMapObjectsStep : BaseStep, IAssemblyModifierPipelineStep -{ - public bool Debug { get; set; } - - public bool ErrorOnCustomJavaObject { get; set; } - - public TaskLoggingHelper Log { get; set; } - - public FindTypeMapObjectsStep (TaskLoggingHelper log) => Log = log; - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - var destinationTypeMapXml = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (context.Destination.ItemSpec); - - // We only care about assemblies that can contains JLOs - if (!context.IsAndroidAssembly) { - Log.LogDebugMessage ($"Skipping assembly '{assembly.Name.Name}' because it is not an Android assembly"); - TypeMapObjectsXmlFile.WriteEmptyFile (destinationTypeMapXml, Log); - return; - } - - var types = ScanForJavaTypes (assembly); - - var xml = new TypeMapObjectsXmlFile { - AssemblyName = assembly.Name.Name, - AssemblyFullName = Debug ? TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name) : null, - AssemblyMvid = assembly.MainModule.Mvid, - }; - - if (Debug) { - var (typeMapDebugSets, foundJniNativeRegistration) = TypeMapCecilAdapter.GetDebugNativeEntries (types, Context, needUniqueAssemblies: false); - - xml.JavaToManagedDebugEntries.AddRange (typeMapDebugSets.JavaToManaged); - xml.ManagedToJavaDebugEntries.AddRange (typeMapDebugSets.ManagedToJava); - xml.FoundJniNativeRegistration = foundJniNativeRegistration; - } else { - var genState = TypeMapCecilAdapter.GetReleaseGenerationState (types, Context, out var foundJniNativeRegistration); - xml.ModuleReleaseData = genState.TempModules.SingleOrDefault ().Value; - } - - xml.Export (destinationTypeMapXml, Log); - } - - List ScanForJavaTypes (AssemblyDefinition assembly) - { - var types = new List (); - - var scanner = new XAJavaTypeScanner (Xamarin.Android.Tools.AndroidTargetArch.None, Log, Context) { - ErrorOnCustomJavaObject = ErrorOnCustomJavaObject - }; - - foreach (ModuleDefinition md in assembly.Modules) { - foreach (TypeDefinition td in md.Types) { - scanner.AddJavaType (td, types); - } - } - - return types; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets index e19d986acda..ee4169c966b 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets @@ -227,10 +227,8 @@ Copyright (C) 2016 Xamarin. All rights reserved. is processed by the ILLink step. If we do not do this then the reference to `netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes. - No TrimmerRootDescriptor is needed: PreTrimmingFixLegacyDesigner rewrites library - assemblies *before* ILLink, replacing designer field loads with calls to the designer - assembly's property getters. ILLink then naturally preserves only the referenced - properties through its mark step, allowing everything else to be trimmed. + The trimmable typemap targets root the designer assembly when trimming is enabled: + resource IDs may be resolved through reflection at runtime. --> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index bb9999a24f1..39b01d08114 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -73,7 +73,6 @@ properties that determine build ordering. _ResolveSatellitePaths; _CreatePackageWorkspace; _LinkAssemblies; - _AfterILLinkAdditionalSteps; _GenerateJavaStubs; _ManifestMerger; _ConvertCustomView; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets deleted file mode 100644 index 74fa387cd41..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ /dev/null @@ -1,454 +0,0 @@ - - - - - - - - - - - - - - - true - <_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag - - <_BeforeAddStaticResources>$(_BeforeAddStaticResources);_GetMonoPlatformJarPath - <_BeforeCompileJava>$(_BeforeCompileJava);_GetMonoPlatformJarPath - <_BeforeCompileToDalvik>$(_BeforeCompileToDalvik);_GetMonoPlatformJarPath - - - - - - - - - <_GenerateJavaStubsDependsOnTargets> - _SetLatestTargetFrameworkVersion; - _PrepareAssemblies; - _PrepareNativeAssemblySources; - $(_AfterPrepareAssemblies); - _ValidateJavaPeerCallbackFormats; - _GetGenerateJavaStubsInputs; - - - - - - - - - - - <_ManifestOutput>$(IntermediateOutputPath)AndroidManifest.xml - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' != 'None'">True - <_LinkingEnabled Condition=" '$(AndroidLinkMode)' == 'None'">False - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' != '' ">True - <_HaveMultipleRIDs Condition=" '$(RuntimeIdentifiers)' == '' ">False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - <_ExtraTrimmerArgs Condition=" '$(_EnableSerializationDiscovery)' != 'false' ">--enable-serialization-discovery $(_ExtraTrimmerArgs) - <_ExtraTrimmerArgs Condition=" '$(_AndroidEnableUnusedTypeChecks)' != 'true' ">--disable-opt unusedtypechecks $(_ExtraTrimmerArgs) - - <_TrimmerDumpDependencies Condition=" '$(LinkerDumpDependencies)' == 'true' ">true - <_AndroidLinkerCustomStepAssembly>$(MSBuildThisFileDirectory)..\tools\Microsoft.Android.Sdk.ILLink.dll - <_SystemIOHashingAssemblyPath>$(MSBuildThisFileDirectory)..\tools\System.IO.Hashing.dll - <_ProguardProjectConfiguration Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)proguard\proguard_project_references.cfg - - - - <_TrimmerCustomData Include="AndroidCustomViewMapFile" Value="$(_OuterCustomViewMapFile)" /> - <_TrimmerCustomData Include="SystemIOHashingAssemblyPath" Value="$(_SystemIOHashingAssemblyPath)" /> - - - - - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveSubStepDispatcher" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.MarkJavaObjects" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.PreserveJavaExceptions" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="MonoDroid.Tuner.PreserveApplications" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveRegistrations" /> - <_TrimmerCustomSteps Include="$(_AndroidLinkerCustomStepAssembly)" Type="Microsoft.Android.Sdk.ILLink.PreserveJavaInterfaces" /> - - - - - - - - - - - - - - - - - - - - - <_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> - - - - - - - - - - - - - - - - - - <_PreTrimmingSwappableItem Include="@(ResolvedFileToPublish)" - Condition=" '%(Extension)' == '.dll' and Exists('$(IntermediateOutputPath)prelink/%(Filename)%(Extension)') " /> - - - - - - - - - - - - - - <_PostTrimmingAssembly Remove="@(_PostTrimmingAssembly)" /> - <_PostTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " /> - - - - - - - - - - - - - - - - <_TypeMapKind Condition=" '$(AndroidIncludeDebugSymbols)' != 'True' ">mvid - <_TypeMapKind Condition=" '$(AndroidIncludeDebugSymbols)' == 'True' ">strings-asm - - - <_PropertyCacheItems Include="TypeMapKind=$(_TypeMapKind)" /> - <_PropertyCacheItems Include="Deterministic=$(Deterministic)" /> - - - - - - - <_AndroidJarAndDexDirectory>$(_XATargetFrameworkDirectories) - - - - - - - - - - - - - - - - - - <_AddTypeManagerResourcesOutput Condition=" '$(_AndroidUseMarshalMethods)' == 'True' ">$(_AndroidIntermediateJavaSourceDirectory)mono\TypeManager.java - <_AddTypeManagerResourcesOutput Condition=" '$(_AndroidUseMarshalMethods)' != 'True' ">$(IntermediateOutputPath)android\bin\mono.android.jar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_GenerateProguardAfterTargets Condition=" '$(_GenerateProguardAfterTargets)' == '' ">ILLink - - - - - <_LinkedAssemblyForProguard Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> - - - - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index a083e26e274..af917b7a586 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -576,14 +576,15 @@ Outputs="@(_BuildTargetAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> + <_TypeMapAssemblySource Include="@(_TypeMapStubAbis->'$(_NativeAssemblySourceDir)typemap.%(Identity).ll')"> + %(_TypeMapStubAbis.Identity) + - - + Debug="$(AndroidIncludeDebugSymbols)" /> @@ -592,10 +593,7 @@ - @@ -204,9 +203,6 @@ - - JavaInteropTypeManager.java - %(Filename)%(Extension) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index b268e1c4b30..2156c1f13f9 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -55,8 +55,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - @@ -64,9 +62,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - - @@ -82,7 +77,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - @@ -269,7 +263,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_NativeAssemblySourceDir>$(IntermediateOutputPath)android\ - <_SkipJniAddNativeMethodRegistrationAttributeScan Condition=" '$(_SkipJniAddNativeMethodRegistrationAttributeScan)' == '' ">False <_AndroidFastDeployEnvironmentFiles Condition=" '$(_AndroidFastDeployEnvironmentFiles)' == '' And '$(EmbedAssembliesIntoApk)' == 'False' ">True <_AndroidFastDeployEnvironmentFiles Condition=" '$(_AndroidFastDeployEnvironmentFiles)' == '' ">False @@ -321,16 +314,6 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidUseAssemblyStore Condition="'$(EmbedAssembliesIntoApk)' != 'true' ">false - - False - - False - - <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' == 'True' ">False - <_AndroidUseMarshalMethods Condition=" '$(AndroidIncludeDebugSymbols)' != 'True' ">$(AndroidEnableMarshalMethods) - - - @@ -495,7 +478,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. /> $(IntermediateOutputPath)link.flag <_AndroidApkPerAbiFlagFile>$(IntermediateOutputPath)android\bin\apk_per_abi.flag <_AndroidDebugKeyStoreFlag>$(IntermediateOutputPath)android_debug_keystore.flag - <_AdditionalPostLinkerStepsFlag>$(_AndroidStampDirectory)_AdditionalPostLinkerSteps.stamp - <_AfterILLinkOutputDir>$(IntermediateOutputPath)afterlink\ <_AcwMapFile>$(IntermediateOutputPath)acw-map.txt <_CustomViewMapFile>$(IntermediateOutputPath)customview-map.txt $(RootNamespace) @@ -951,7 +932,6 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" /> <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> <_PropertyCacheItems Include="AndroidTypeMapImplementation=$(AndroidTypeMapImplementation)" /> - <_PropertyCacheItems Include="_AndroidUseMarshalMethods=$(_AndroidUseMarshalMethods)" /> <_PropertyCacheItems Include="_AndroidJcwCodegenTarget=$(_AndroidJcwCodegenTarget)" /> <_PropertyCacheItems Include="_AndroidAssemblyStoreCompressionLevel=$(_AndroidAssemblyStoreCompressionLevel)" /> @@ -1354,17 +1334,9 @@ because xbuild doesn't support framework reference assemblies. - - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime.jar - - - + <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar - - - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_clr.jar - <_AllResolvedAssemblies Include="@(ResolvedAssemblies)" /> - - $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty - $(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty - @@ -1414,7 +1378,6 @@ because xbuild doesn't support framework reference assemblies. ApplicationJavaClass="$(AndroidApplicationJavaClass)" CodeGenerationTarget="$(AndroidCodegenTarget)" Debug="$(AndroidIncludeDebugSymbols)" - EnableMarshalMethods="$(_AndroidUseMarshalMethods)" ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" PackageNamingPolicy="$(AndroidPackageNamingPolicy)" ResolvedAssemblies="@(_AllResolvedAssemblies)" @@ -1434,77 +1397,6 @@ because xbuild doesn't support framework reference assemblies. - - - - - <_AfterILLinkSourceFiles Include="@(ResolvedAssemblies)" - Condition=" '$(_AndroidUseMarshalMethods)' != 'True' or !$([System.String]::Copy('%(ResolvedAssemblies.Filename)').ToLowerInvariant().EndsWith('.resources')) " /> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' != 'True' " Include="@(_AfterILLinkSourceFiles->'$(_AfterILLinkOutputDir)%(DestinationSubPath)')" /> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' == 'True' " Include="@(_AfterILLinkSourceFiles)" /> - <_AndroidScanMarkerFiles Include="@(ResolvedAssemblies->'%(JavaObjectsXmlFile)')" Condition=" '%(ResolvedAssemblies.JavaObjectsXmlFile)' != '' " /> - - - - - - - - - <_AfterILLinkSourceFiles Remove="@(_AfterILLinkSourceFiles)" /> - <_AfterILLinkDestFiles Remove="@(_AfterILLinkDestFiles)" /> - <_AndroidScanMarkerFiles Remove="@(_AndroidScanMarkerFiles)" /> - - - - - - - - - - <_OrigResolvedAssemblies Include="@(ResolvedAssemblies)" /> - - - - <_OrigResolvedUserAssemblies Include="@(ResolvedUserAssemblies)" /> - - - - <_OrigResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> - - - - <_OrigResolvedAssemblies Remove="@(_OrigResolvedAssemblies)" /> - <_OrigResolvedUserAssemblies Remove="@(_OrigResolvedUserAssemblies)" /> - <_OrigResolvedFrameworkAssemblies Remove="@(_OrigResolvedFrameworkAssemblies)" /> - - - @@ -1763,7 +1655,6 @@ because xbuild doesn't support framework reference assemblies. AndroidPackageName="$(_AndroidPackage)" PackageNamingPolicy="$(AndroidPackageNamingPolicy)" UseAssemblyStore="$(_AndroidUseAssemblyStore)" - EnableMarshalMethods="$(_AndroidUseMarshalMethods)" CustomBundleConfigFile="$(AndroidBundleConfigurationFile)" AndroidRuntime="$(_AndroidRuntime)" ProjectRuntimeConfigFilePath="$(ProjectRuntimeConfigFilePath)" @@ -1773,12 +1664,9 @@ because xbuild doesn't support framework reference assemblies. @@ -2994,7 +2882,5 @@ because xbuild doesn't support framework reference assemblies. - diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 40879cbfd01..cce5f669ba5 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -883,20 +883,10 @@ public void ActivityAliasRuns ([ValueSource (typeof (BaseTest), nameof (BaseTest } [Test] - public void NativeAssemblyCacheWithSatelliteAssemblies ([Values] bool enableMarshalMethods, [Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) + public void NativeAssemblyCacheWithSatelliteAssemblies ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) { const bool isRelease = true; - if (enableMarshalMethods && runtime == AndroidRuntime.CoreCLR) { - // This currently fails with the following exception: - // - // error XARMM7015: System.NotSupportedException: Writing mixed-mode assemblies is not supported - // at Mono.Cecil.ModuleWriter.Write(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleWriter.WriteModule(ModuleDefinition module, Disposable`1 stream, WriterParameters parameters) - // at Mono.Cecil.ModuleDefinition.Write(String fileName, WriterParameters parameters) - // at Mono.Cecil.AssemblyDefinition.Write(String fileName, WriterParameters parameters) - // at Xamarin.Android.Tasks.MarshalMethodsAssemblyRewriter.Rewrite(Boolean brokenExceptionTransitions) in src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs:line 165 - // at Xamarin.Android.Tasks.RewriteMarshalMethods.RewriteMethods(NativeCodeGenState state, Boolean brokenExceptionTransitionsEnabled) in src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs:line 160 - Assert.Ignore ("CoreCLR: fails because of a Mono.Cecil lack of support"); + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } @@ -923,7 +913,6 @@ public void NativeAssemblyCacheWithSatelliteAssemblies ([Values] bool enableMars var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) { IsRelease = isRelease, - EnableMarshalMethods = enableMarshalMethods, }; proj.SetRuntime (runtime); proj.References.Add (new BuildItem.ProjectReference ($"..\\{lib.ProjectName}\\{lib.ProjectName}.csproj", lib.ProjectName, lib.ProjectGuid)); diff --git a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs b/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs deleted file mode 100644 index ca817105d4d..00000000000 --- a/tests/MSBuildDeviceIntegration/Tests/MarshalMethodsGCHangTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using NUnit.Framework; -using Xamarin.ProjectTools; -using System.IO; -using System.Linq; -using Microsoft.Build.Framework; -using System.Text; -using System.Xml.Linq; -using System.Collections.Generic; -using Xamarin.Android.Tasks; - -namespace Xamarin.Android.Build.Tests; - -[TestFixture] -[Category ("UsesDevice")] -[Category ("MayHang")] -public class MarshalMethodsGCHangTests : DeviceTest -{ - static readonly string MarshalMethodsAppRuns_PermissionManifest = @" - - - - - -"; - - static readonly string MarshalMethodsAppRuns_MainActivity = @"using Android.Media; - -namespace marshal2; - -[Activity (Label = ""@string/app_name"", MainLauncher = true)] -public class MainActivity : Activity -{ - protected override void OnCreate (Bundle? savedInstanceState) - { - base.OnCreate (savedInstanceState); - SetContentView (Resource.Layout.Main); - } - - protected override void OnStart () - { - base.OnStart (); - - try { - var mp = new MediaPlayer (); - mp.SetDataSource (new StreamMediaDataSource (new MemoryStream (new byte[65536]))); - mp.Prepare (); - } catch (Java.IO.IOException) { - GC.Collect (); - } - Console.WriteLine (""XXX:OnStart done""); - } - - class StreamMediaDataSource (System.IO.Stream data) : MediaDataSource - { - public override long Size => data.Length; - - public override int ReadAt (long position, byte[]? buffer, int offset, int size) - { - try { - Console.WriteLine ($""XXX:START StreamMediaDataSource.ReadAt {position} {buffer} {buffer?.Length ?? 0} {offset} {size}""); - - // Allocate enough to trigger GC - for (int i = 0; i < 1000; i++) { - _ = new byte[8192]; - } - - if (data.CanSeek) { - data.Seek (position, SeekOrigin.Begin); - } - return data.Read (buffer ?? [], offset, size); - } finally { - Console.WriteLine ($""XXX:END //StreamMediaDataSource.ReadAt {position} {buffer} {buffer?.Length ?? 0} {offset} {size}""); - } - } - - public override void Close () - { - data.Dispose (); - data = System.IO.Stream.Null; - } - } -} -"; - - [Test] - [Ignore ("Marshal methods are not yet working on CoreCLR: https://github.com/dotnet/android/issues/12206")] - public void MarshalMethodsAppRuns ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) - { - const bool isRelease = true; - if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { - return; - } - - if (runtime == AndroidRuntime.NativeAOT) { - Assert.Ignore ("Not supported with NativeAOT"); - } - - var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "marshal2")) { - IsRelease = isRelease, - EnableMarshalMethods = true, - SupportedOSPlatformVersion = "24", // Minimum 24 to be able to test on Android 16 - TrimModeRelease = TrimMode.Full, - ProjectName = "marshal2", - }; - proj.SetRuntime (runtime); - proj.SetRuntimeIdentifiers (new[] { DeviceAbi }); - proj.AndroidManifest = String.Format (MarshalMethodsAppRuns_PermissionManifest, proj.PackageName); - proj.MainActivity = MarshalMethodsAppRuns_MainActivity; - proj.SetDefaultTargetDevice (); - // TODO: AndroidEnableMarshalMethods and ReadyToRun do not work together - if (runtime == AndroidRuntime.CoreCLR) { - proj.SetProperty ("PublishReadyToRun", "false"); - } - - using var apkBuilder = CreateApkBuilder (); - Assert.True (apkBuilder.Install (proj), "Project should have installed."); - ClearAdbLogcat (); - - const string expectedLogcatOutput = "XXX:OnStart done"; - Assert.IsTrue ( - MonitorAdbLogcat ( - InstallAndRunTests.CreateLineChecker (expectedLogcatOutput), - logcatFilePath: Path.Combine (Root, apkBuilder.ProjectDirectory, "startup-logcat.log"), - timeout: 60, - onMonitoringStarted: () => StartActivityAndAssert (proj) - ), - $"Output did not contain {expectedLogcatOutput}!" - ); - } -} From ce2b7ca55e6702b4bd700346a4236f84bd38708f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 08:44:15 +0200 Subject: [PATCH 41/58] Preserve library Java runtime jar selection Use trimmable Java APIs for explicit library opt-ins without importing application type map targets, and retain the packaged CoreCLR/default jars for libraries without an implementation selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Common.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 2156c1f13f9..6075d981d88 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1335,7 +1335,9 @@ because xbuild doesn't support framework reference assemblies. Trimmable jars replace JavaProxyObject/JavaProxyThrowable with non-native implementations. --> - <_RuntimeJar>$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' == 'trimmable' ">$(MSBuildThisFileDirectory)\java_runtime_trimmable.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' != 'trimmable' and '$(_AndroidRuntime)' == 'CoreCLR' ">$(MSBuildThisFileDirectory)\java_runtime_clr.jar + <_RuntimeJar Condition=" '$(AndroidTypeMapImplementation)' != 'trimmable' and '$(_AndroidRuntime)' != 'CoreCLR' ">$(MSBuildThisFileDirectory)\java_runtime.jar From ee9dc12baf3ca06016863cd78c87461e13c88c5e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 15:34:34 +0200 Subject: [PATCH 42/58] Restore CoreCLR remote-process provider generation Keep generating additional Java runtime providers for trimmable CoreCLR apps after removing the legacy GenerateAdditionalProviderSources task. Register a dedicated task and track generated Java files for incremental Clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index af917b7a586..833f47a99e7 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -545,6 +545,11 @@ OutputDirectory="$(IntermediateOutputPath)android" AdditionalProviderSources="@(_AdditionalProviderSources)" /> + + Date: Wed, 30 Sep 2026 18:11:03 +0200 Subject: [PATCH 43/58] Remove duplicate provider invocation after main rebase The shared CoreCLR provider generation is already supplied by main. Keep a single call while preserving the legacy typemap removals and upstream prelink fixes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 833f47a99e7..af917b7a586 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -545,11 +545,6 @@ OutputDirectory="$(IntermediateOutputPath)android" AdditionalProviderSources="@(_AdditionalProviderSources)" /> - - Date: Wed, 30 Sep 2026 23:27:23 +0200 Subject: [PATCH 44/58] Fix remaining trimmable typemap CI regressions Exercise the supported typemap validation target instead of deleted build targets. Remove obsolete Cecil JCW/XML scanning from the no-shrink pipeline, retaining compatibility fixups and assembly copying, and update managed-typemap regression coverage. Isolate relocation-test builds from persistent compiler and MSBuild servers without weakening move, incremental-build, or signing assertions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../MonoDroid.Tuner/FindJavaObjectsStep.cs | 119 -------------- .../Tasks/AssemblyModifierPipeline.cs | 14 -- .../Xamarin.Android.Build.Tests/BuildTest2.cs | 13 +- .../BuildWithLibraryTests.cs | 21 +-- .../InvalidConfigTests.cs | 11 +- .../Tasks/LinkerTests.cs | 58 ++++++- .../Utilities/ACWMapGenerator.cs | 151 ------------------ .../Utilities/JavaObjectsXmlFile.cs | 148 ----------------- .../Utilities/XAJavaTypeScanner.cs | 135 ---------------- .../Xamarin.Android.Build.Tasks.csproj | 1 - .../Xamarin.Android.Common.targets | 4 +- 11 files changed, 78 insertions(+), 597 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/ACWMapGenerator.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/JavaObjectsXmlFile.cs delete mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/XAJavaTypeScanner.cs diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs deleted file mode 100644 index 269e8f152b4..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Java.Interop.Tools.JavaCallableWrappers; -using Java.Interop.Tools.JavaCallableWrappers.Adapters; -using Java.Interop.Tools.JavaCallableWrappers.CallableWrapperMembers; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Utilities; -using Mono.Cecil; -using Mono.Linker; -using Mono.Linker.Steps; -using Xamarin.Android.Tasks; - -namespace MonoDroid.Tuner; - -/// -/// Scans an assembly for JLOs that need JCWs generated and writes them to an XML file. -/// -public class FindJavaObjectsStep : BaseStep, IAssemblyModifierPipelineStep -{ - public string ApplicationJavaClass { get; set; } = ""; - - public bool ErrorOnCustomJavaObject { get; set; } - - public TaskLoggingHelper Log { get; set; } - - public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log; - - public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) - { - var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (context.Destination.ItemSpec); - var scanned = ScanAssembly (assembly, context, destinationJLOXml); - - if (!scanned) { - // We didn't scan for Java objects, so write an empty .xml file for later steps - JavaObjectsXmlFile.WriteEmptyFile (destinationJLOXml, Log); - } - } - - public bool ScanAssembly (AssemblyDefinition assembly, StepContext context, string destinationJLOXml) - { - if (!ShouldScan (context)) - return false; - - var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip; - - if (action == AssemblyAction.Delete) - return false; - - var types = ScanForJavaTypes (assembly); - var initial_count = types.Count; - - // Filter out Java types we don't care about - types = types.Where (t => !JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (t, Context)).ToList (); - - Log.LogDebugMessage ($"{assembly.Name.Name} - Found {initial_count} Java types, filtered to {types.Count}"); - - var xml = new JavaObjectsXmlFile (); - - xml.ACWMapEntries.AddRange (types.Select (t => ACWMapEntry.Create (t, Context))); - xml.JavaCallableWrappers.AddRange (ConvertToCallableWrappers (types.Where (t => !t.IsInterface).ToList ())); - - xml.Export (destinationJLOXml); - - Log.LogDebugMessage ($"Wrote '{destinationJLOXml}', {xml.JavaCallableWrappers.Count} JCWs, {xml.ACWMapEntries.Count} ACWs"); - - return true; - } - - bool ShouldScan (StepContext context) - { - if (!context.IsAndroidAssembly) - return false; - - // XAJavaInterop1 uses the framework JCWs from the runtime jar. - var shouldSkipNonUserAssemblies = context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1; - - if (shouldSkipNonUserAssemblies && !context.IsUserAssembly) { - Log.LogDebugMessage ($"Skipping assembly '{context.Source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies"); - return false; - } - - return true; - } - - List ScanForJavaTypes (AssemblyDefinition assembly) - { - var types = new List (); - - var scanner = new XAJavaTypeScanner (Xamarin.Android.Tools.AndroidTargetArch.None, Log, Context) { - ErrorOnCustomJavaObject = ErrorOnCustomJavaObject - }; - - foreach (ModuleDefinition md in assembly.Modules) { - foreach (TypeDefinition td in md.Types) { - scanner.AddJavaType (td, types); - } - } - - return types; - } - - List ConvertToCallableWrappers (List types) - { - var wrappers = new List (); - - var reader_options = new CallableWrapperReaderOptions { - DefaultApplicationJavaClass = ApplicationJavaClass, - DefaultMonoRuntimeInitialization = "mono.MonoPackageManager.LoadApplication (context);", - }; - - foreach (var type in types) { - var wrapper = CecilImporter.CreateType (type, Context, reader_options); - wrappers.Add (wrapper); - } - - return wrappers; - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs index d16a3bbcf99..be4e2de00e9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs @@ -26,8 +26,6 @@ public class AssemblyModifierPipeline : AndroidTask { public override string TaskPrefix => "AMP"; - public string ApplicationJavaClass { get; set; } = ""; - public string CodeGenerationTarget { get; set; } = ""; public bool Debug { get; set; } @@ -37,8 +35,6 @@ public class AssemblyModifierPipeline : AndroidTask public bool Deterministic { get; set; } - public bool ErrorOnCustomJavaObject { get; set; } - public string? PackageNamingPolicy { get; set; } /// @@ -128,15 +124,6 @@ public override bool RunTask () protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) { - // FindJavaObjectsStep - var findJavaObjectsStep = new FindJavaObjectsStep (Log) { - ApplicationJavaClass = ApplicationJavaClass, - ErrorOnCustomJavaObject = ErrorOnCustomJavaObject, - }; - - findJavaObjectsStep.Initialize (context); - pipeline.Steps.Add (findJavaObjectsStep); - // SaveChangedAssemblyStep var writerParameters = new WriterParameters { DeterministicMvid = Deterministic, @@ -144,7 +131,6 @@ protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkCont var saveChangedAssemblyStep = new SaveChangedAssemblyStep (Log, writerParameters); pipeline.Steps.Add (saveChangedAssemblyStep); - } void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 22ace1122ea..effcc029672 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -186,11 +186,16 @@ public void BuildBasicApplicationThenMoveIt ([ValueSource (typeof (BaseTest), na IsRelease = isRelease, }; proj.SetRuntime (runtime); + // This test relocates build inputs; no compiler/build server may retain them. + proj.SetProperty ("UseSharedCompilation", "false"); + var environmentVariables = new Dictionary { + ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "0", + }; using (var b = CreateApkBuilder (path)) { b.Target = "Build"; - Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + Assert.IsTrue (b.Build (proj, environmentVariables: environmentVariables), "Build should have succeeded."); b.Target = "SignAndroidPackage"; - Assert.IsTrue (b.Build (proj), "SignAndroidPackage should have succeeded."); + Assert.IsTrue (b.Build (proj, environmentVariables: environmentVariables), "SignAndroidPackage should have succeeded."); string path2 = Path.Combine (Root, "temp", TestName, "App2"); @@ -201,11 +206,11 @@ public void BuildBasicApplicationThenMoveIt ([ValueSource (typeof (BaseTest), na foreach (var r in proj.AndroidResources) r.Timestamp = DateTime.UtcNow; b.Target = "Build"; - Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Build should have succeeded."); + Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, environmentVariables: environmentVariables), "Build should have succeeded."); Assert.IsTrue (!b.Output.IsTargetSkipped ("_CleanIntermediateIfNeeded"), "_CleanIntermediateIfNeeded should be built."); Assert.IsTrue (!b.Output.IsTargetSkipped ("_CompileResources"), "_CompileResources Should have built."); b.Target = "SignAndroidPackage"; - Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "SignAndroidPackage should have succeeded."); + Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, environmentVariables: environmentVariables), "SignAndroidPackage should have succeeded."); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index 61b8881b4dc..764e2b7294d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -1111,8 +1111,7 @@ public void DotNetLibraryAarChanges ([Values (AndroidRuntime.CoreCLR, AndroidRun /// And MultiTfmLib contains Java-interop types (e.g. BroadcastReceiver) that only /// exist in the net11.0-android TFM, the build tasks must load the Android-TFM assembly /// and generate JCWs for those types. Previously, the net11.0 (non-Android) assembly - /// could be loaded instead, causing FindJavaObjectsStep to report "Found 0 Java types" - /// and producing empty .jlo.xml files. + /// could be loaded instead, omitting Android-only types from the managed type map. /// [Test] public void MultiTfmTransitiveReference ([Values (AndroidRuntime.CoreCLR)] AndroidRuntime runtime) @@ -1209,18 +1208,12 @@ public class CoreClass using var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName)); Assert.IsTrue (appBuilder.Build (app), $"{app.ProjectName} should build"); - // Verify: MultiTfmLib.jlo.xml should NOT be empty (i.e. the assembly was scanned as an Android assembly) - var jloXml = appBuilder.Output.GetIntermediaryPath ( - Path.Combine ("android", "assets", "arm64-v8a", "MultiTfmLib.jlo.xml")); - FileAssert.Exists (jloXml); - - var jloXmlInfo = new FileInfo (jloXml); - Assert.IsTrue (jloXmlInfo.Length > 0, - "MultiTfmLib.jlo.xml should not be empty — the Android-TFM assembly was not loaded (wrong TFM loaded instead)"); - - var jloContent = File.ReadAllText (jloXml); - Assert.IsTrue (jloContent.Contains ("MyReceiver"), - $"MultiTfmLib.jlo.xml should contain the MyReceiver JCW type, but got: {jloContent}"); + var typeMapPath = appBuilder.Output.GetIntermediaryPath ( + Path.Combine ("typemap", "_MultiTfmLib.TypeMap.dll")); + FileAssert.Exists (typeMapPath, "The Android-TFM library should have a managed type map."); + using var typeMap = Mono.Cecil.AssemblyDefinition.ReadAssembly (typeMapPath); + Assert.IsTrue (typeMap.MainModule.GetTypeReferences ().Any (type => type.FullName == "MultiTfmLib.MyReceiver"), + "The managed type map should reference the Android-only MyReceiver type."); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs index 6861b0f90f2..b0192a921e7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/InvalidConfigTests.cs @@ -120,17 +120,14 @@ public void UnsupportedTypeMapIsRejected (bool isApplication, string typeMapImpl [TestCase (false, "Build")] [TestCase (true, "Publish")] [TestCase (false, "Publish")] - [TestCase (true, "_GenerateJavaStubs")] - [TestCase (true, "_PrepareLinking")] + [TestCase (true, "_ValidateAndroidTypeMapImplementation")] + [TestCase (false, "_ValidateAndroidTypeMapImplementation")] public void LegacyTypeMapIsRejectedBeforeBuildTargets (bool isApplication, string target) { XamarinProject project = isApplication ? new XamarinAndroidApplicationProject () : new XamarinAndroidLibraryProject (); project.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); - if (target == "_PrepareLinking") { - project.SetProperty ("PublishTrimmed", "true"); - } using var builder = isApplication ? CreateApkBuilder () : CreateDllBuilder (); builder.Target = target; @@ -145,8 +142,8 @@ public void LegacyTypeMapIsRejectedBeforeBuildTargets (bool isApplication, strin [TestCase (false, "Build")] [TestCase (true, "Publish")] [TestCase (false, "Publish")] - [TestCase (true, "_GenerateJavaStubs")] - [TestCase (true, "_PrepareLinking")] + [TestCase (true, "_ValidateAndroidTypeMapImplementation")] + [TestCase (false, "_ValidateAndroidTypeMapImplementation")] public void EmptyGlobalTypeMapIsRejected (bool isApplication, string target) { XamarinProject project = isApplication diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index 22c0fc57419..812ac5f1a22 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -46,7 +46,8 @@ public void LinkAssembliesNoShrinkLegacyCompatibilityFixups (bool enabled) $"{nameof (FixLegacyResourceDesignerStep)} presence should match the compatibility fixup setting."); Assert.AreEqual (enabled, pipeline.Steps.Any (step => step is AddKeepAlivesStep), $"{nameof (AddKeepAlivesStep)} presence should match the compatibility fixup setting."); - Assert.IsTrue (pipeline.Steps.Any (step => step is FindJavaObjectsStep), $"{nameof (FindJavaObjectsStep)} should always run."); + Assert.AreEqual (enabled ? 4 : 1, pipeline.Steps.Count, + "Only requested compatibility fixups and the assembly-copy step should run; JCW generation is handled by the trimmable generator."); Assert.IsTrue (pipeline.Steps.Any (step => step is SaveChangedAssemblyStep), $"{nameof (SaveChangedAssemblyStep)} should always run."); } @@ -56,6 +57,61 @@ public void BuildPipelineForTest (AssemblyPipeline pipeline, MSBuildLinkContext BuildPipeline (pipeline, context); } + [Test] + public void LinkAssembliesNoShrinkCopiesJavaPeersWithoutLegacyImport () + { + var path = Path.Combine (Root, "temp", TestName); + Directory.CreateDirectory (path); + try { + var androidPath = Path.Combine (path, "Mono.Android.dll"); + var assemblyPath = Path.Combine (path, "App.dll"); + var destinationPath = Path.Combine (path, "linked", "App.dll"); + using (var android = CreateFauxMonoAndroidAssembly ()) + using (var assembly = AssemblyDefinition.CreateAssembly (new AssemblyNameDefinition ("App", new Version ()), "App", ModuleKind.Dll)) { + android.Write (androidPath); + var module = assembly.MainModule; + var peer = new TypeDefinition ("Example", "Peer", TypeAttributes.Public, + module.ImportReference (android.MainModule.GetType ("Java.Lang.Object"))); + module.Types.Add (peer); + var method = new MethodDefinition ("Callback", MethodAttributes.Public | MethodAttributes.Virtual, module.TypeSystem.Void); + method.Body.Instructions.Add (Instruction.Create (OpCodes.Ret)); + var registerType = new TypeReference ("Android.Runtime", "RegisterAttribute", module, peer.BaseType.Scope); + var registerConstructor = new MethodReference (".ctor", module.TypeSystem.Void, registerType) { HasThis = true }; + registerConstructor.Parameters.Add (new ParameterDefinition (module.TypeSystem.String)); + var register = new CustomAttribute (registerConstructor); + register.ConstructorArguments.Add (new CustomAttributeArgument (module.TypeSystem.String, "callback")); + method.CustomAttributes.Add (register); + peer.Methods.Add (method); + assembly.Write (assemblyPath); + } + + var source = new TaskItem (assemblyPath); + source.SetMetadata ("Abi", "arm64-v8a"); + source.SetMetadata ("TargetFrameworkIdentifier", "MonoAndroid"); + var destination = new TaskItem (destinationPath); + destination.SetMetadata ("Abi", "arm64-v8a"); + var androidItem = new TaskItem (androidPath); + androidItem.SetMetadata ("Abi", "arm64-v8a"); + var task = new LinkAssembliesNoShrink { + BuildEngine = new MockBuildEngine (TestContext.Out), + CodeGenerationTarget = "XAJavaInterop1", + DestinationFiles = [destination], + EnableLegacyCompatibilityAssemblyFixups = false, + ResolvedAssemblies = [source, androidItem], + ResolvedUserAssemblies = [source], + SourceFiles = [source], + TargetName = "App", + }; + + Assert.IsTrue (task.Execute (), "Assembly copying must not reimport Java peers using legacy JNI signatures or connectors."); + CollectionAssert.AreEqual (File.ReadAllBytes (assemblyPath), File.ReadAllBytes (destinationPath)); + Assert.IsFalse (File.Exists (Path.ChangeExtension (destinationPath, ".jlo.xml")), + "Legacy Java-object XML should not be generated."); + } finally { + Directory.Delete (path, recursive: true); + } + } + [Test] public void PostTrimmingSkipsLegacyAbstractFixupsButWarnsAboutAppDomain () { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ACWMapGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ACWMapGenerator.cs deleted file mode 100644 index 625e49aa5d7..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ACWMapGenerator.cs +++ /dev/null @@ -1,151 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml.Linq; -using Java.Interop.Tools.Cecil; -using Java.Interop.Tools.TypeNameMappings; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Utilities; -using Mono.Cecil; - -namespace Xamarin.Android.Tasks; - -class ACWMapGenerator -{ - readonly TaskLoggingHelper log; - - public ACWMapGenerator (TaskLoggingHelper log) - { - this.log = log; - } - - public void Generate (List javaTypes, string acwMapFile) - { - // We need to save a map of .NET type -> ACW type for resource file fixups - var managed = new Dictionary (javaTypes.Count, StringComparer.Ordinal); - var java = new Dictionary (javaTypes.Count, StringComparer.Ordinal); - - var managedConflicts = new Dictionary> (0, StringComparer.Ordinal); - var javaConflicts = new Dictionary> (0, StringComparer.Ordinal); - - using var acw_map = MemoryStreamPool.Shared.CreateStreamWriter (); - - foreach (var type in javaTypes.OrderBy (t => t.ManagedKey)) { - string managedKey = type.ManagedKey; - string javaKey = type.JavaKey; - - acw_map.Write (type.PartialAssemblyQualifiedName); - acw_map.Write (';'); - acw_map.Write (javaKey); - acw_map.WriteLine (); - - ACWMapEntry conflict; - bool hasConflict = false; - - if (managed.TryGetValue (managedKey, out conflict)) { - if (!conflict.ModuleName.Equals (type.ModuleName)) { - if (!managedConflicts.TryGetValue (managedKey, out var list)) - managedConflicts.Add (managedKey, list = new List { conflict.PartialAssemblyName }); - list.Add (type.PartialAssemblyName); - } - hasConflict = true; - } - - if (java.TryGetValue (javaKey, out conflict)) { - if (!conflict.ModuleName.Equals(type.ModuleName)) { - if (!javaConflicts.TryGetValue (javaKey, out var list)) - javaConflicts.Add (javaKey, list = new List { conflict.AssemblyQualifiedName }); - list.Add (type.AssemblyQualifiedName); - } - hasConflict = true; - } - - if (!hasConflict) { - managed.Add (managedKey, type); - java.Add (javaKey, type); - - acw_map.Write (managedKey); - acw_map.Write (';'); - acw_map.Write (javaKey); - acw_map.WriteLine (); - - acw_map.Write (type.CompatJniName); - acw_map.Write (';'); - acw_map.Write (javaKey); - acw_map.WriteLine (); - } - } - - acw_map.Flush (); - - foreach (var kvp in managedConflicts) { - log.LogCodedWarning ("XA4214", Properties.Resources.XA4214, kvp.Key, string.Join (", ", kvp.Value)); - log.LogCodedWarning ("XA4214", Properties.Resources.XA4214_Result, kvp.Key, kvp.Value [0]); - } - - foreach (var kvp in javaConflicts) { - log.LogCodedError ("XA4215", Properties.Resources.XA4215, kvp.Key); - - foreach (var typeName in kvp.Value) { - log.LogCodedError ("XA4215", Properties.Resources.XA4215_Details, kvp.Key, typeName); - } - } - - // Don't write the output file if there are any errors so that - // future incremental builds will try again. - if (javaConflicts.Count > 0) - return; - - Files.CopyIfStreamChanged (acw_map.BaseStream, acwMapFile); - } -} - -class ACWMapEntry -{ - public string AssemblyQualifiedName { get; set; } - public string CompatJniName { get; set; } - public string JavaKey { get; set; } - public string ManagedKey { get; set; } - public string ModuleName { get; set; } - public string PartialAssemblyName { get; set; } - public string PartialAssemblyQualifiedName { get; set; } - - public ACWMapEntry (string assemblyQualifiedName, string compatJniName, string javaKey, string managedKey, string moduleName, string partialAssemblyName, string partialAssemblyQualifiedName) - { - AssemblyQualifiedName = assemblyQualifiedName; - CompatJniName = compatJniName; - JavaKey = javaKey; - ManagedKey = managedKey; - ModuleName = moduleName; - PartialAssemblyName = partialAssemblyName; - PartialAssemblyQualifiedName = partialAssemblyQualifiedName; - } - - public static ACWMapEntry Create (TypeDefinition type, TypeDefinitionCache cache) - { - return new ACWMapEntry ( - assemblyQualifiedName: type.GetAssemblyQualifiedName (cache), - compatJniName: JavaNativeTypeManager.ToCompatJniName (type, cache).Replace ('/', '.'), - javaKey: JavaNativeTypeManager.ToJniName (type, cache).Replace ('/', '.'), - managedKey: type.FullName.Replace ('/', '.'), - moduleName: type.Module.Name, - partialAssemblyName: type.GetPartialAssemblyName (cache), - partialAssemblyQualifiedName: type.GetPartialAssemblyQualifiedName (cache) - ); - } - - public static ACWMapEntry Create (XElement type, string partialAssemblyName, string moduleName) - { - return new ACWMapEntry ( - assemblyQualifiedName: type.GetAttributeOrDefault ("assembly-qualified-name", string.Empty), - compatJniName: type.GetAttributeOrDefault ("compat-jni-name", string.Empty), - javaKey: type.GetAttributeOrDefault ("java-key", string.Empty), - managedKey: type.GetAttributeOrDefault ("managed-key", string.Empty), - moduleName: moduleName, - partialAssemblyName: partialAssemblyName, - partialAssemblyQualifiedName: type.GetAttributeOrDefault ("partial-assembly-qualified-name", string.Empty) - ); - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JavaObjectsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JavaObjectsXmlFile.cs deleted file mode 100644 index d3e51243322..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JavaObjectsXmlFile.cs +++ /dev/null @@ -1,148 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; -using Java.Interop.Tools.JavaCallableWrappers.Adapters; -using Java.Interop.Tools.JavaCallableWrappers.CallableWrapperMembers; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Xamarin.Android.Tasks; - -class JavaObjectsXmlFile -{ - static readonly XmlWriterSettings settings = new XmlWriterSettings { - Indent = true, - NewLineOnAttributes = false, - OmitXmlDeclaration = true, - }; - - static readonly JavaObjectsXmlFile unscanned = new JavaObjectsXmlFile { WasScanned = false }; - - public List ACWMapEntries { get; } = []; - public List JavaCallableWrappers { get; } = []; - public bool WasScanned { get; private set; } - - public void Export (string filename) - { - using var sw = MemoryStreamPool.Shared.CreateStreamWriter (); - - using (var xml = XmlWriter.Create (sw, settings)) - Export (xml); - - sw.Flush (); - - Files.CopyIfStreamChanged (sw.BaseStream, filename); - } - - void Export (XmlWriter xml) - { - xml.WriteStartElement ("api"); - - ExportJCWTypes (xml); - ExportACWMappingTypes (xml); - - xml.WriteEndElement (); - } - - void ExportJCWTypes (XmlWriter xml) - { - if (JavaCallableWrappers.Count == 0) - return; - - xml.WriteStartElement ("jcw-types"); - - XmlExporter.Export (xml, JavaCallableWrappers); - - xml.WriteEndElement (); - } - - void ExportACWMappingTypes (XmlWriter xml) - { - if (ACWMapEntries.Count == 0) - return; - - var t = ACWMapEntries.First (); - - xml.WriteStartElement ("acw-types"); - - xml.WriteAttributeStringIfNotDefault ("partial-assembly-name", t.PartialAssemblyName); - xml.WriteAttributeStringIfNotDefault ("module-name", t.ModuleName); - - foreach (var type in ACWMapEntries) { - xml.WriteStartElement ("type"); - xml.WriteAttributeStringIfNotDefault ("assembly-qualified-name", type.AssemblyQualifiedName); - xml.WriteAttributeStringIfNotDefault ("compat-jni-name", type.CompatJniName); - xml.WriteAttributeStringIfNotDefault ("java-key", type.JavaKey); - xml.WriteAttributeStringIfNotDefault ("managed-key", type.ManagedKey); - xml.WriteAttributeStringIfNotDefault ("partial-assembly-qualified-name", type.PartialAssemblyQualifiedName); - xml.WriteEndElement (); - } - - xml.WriteEndElement (); - } - - /// - /// Given an assembly path, return the path to the ".jlo.xml" file that should be next to it. - /// - public static string GetJavaObjectsXmlFilePath (string assemblyPath) - => Path.ChangeExtension (assemblyPath, ".jlo.xml"); - - public static string GetJavaObjectsXmlFilePath (ITaskItem assembly) - { - var path = assembly.GetMetadata ("JavaObjectsXmlFile"); - return path.IsNullOrEmpty () ? GetJavaObjectsXmlFilePath (assembly.ItemSpec) : path; - } - - public static JavaObjectsXmlFile Import (string filename, JavaObjectsXmlFileReadType readType) - { - // If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs. - // This check is much faster than loading and parsing an empty XML file. - var fi = new FileInfo (filename); - - if (fi.Length == 0) - return unscanned; - - var file = new JavaObjectsXmlFile { - WasScanned = true, - }; - - var xml = XDocument.Load (filename); - - // We let callers specify which part(s) of the file they want to deserialize to save time - if (readType.HasFlag (JavaObjectsXmlFileReadType.JavaCallableWrappers) && xml.Root?.Element ("jcw-types") is XElement jcw) - file.JavaCallableWrappers.AddRange (XmlImporter.Import (jcw)); - - if (readType.HasFlag (JavaObjectsXmlFileReadType.AndroidResourceFixups) && xml.Root?.Element ("acw-types") is XElement acw) { - var partialAssemblyName = acw.GetAttributeOrDefault ("partial-assembly-name", string.Empty); - var moduleName = acw.GetAttributeOrDefault ("module-name", string.Empty); - - foreach (var type in acw.Elements ("type")) { - var entry = ACWMapEntry.Create (type, partialAssemblyName, moduleName); - file.ACWMapEntries.Add (entry); - } - } - - return file; - } - - public static void WriteEmptyFile (string destination, TaskLoggingHelper log) - { - log.LogDebugMessage ($"Writing empty file '{destination}'"); - - // We write a zero byte file to indicate the file couldn't have JLO types and wasn't scanned - File.Create (destination).Dispose (); - } -} - -[Flags] -enum JavaObjectsXmlFileReadType -{ - None = 0, - AndroidResourceFixups = 1, - JavaCallableWrappers = 2, -} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/XAJavaTypeScanner.cs b/src/Xamarin.Android.Build.Tasks/Utilities/XAJavaTypeScanner.cs deleted file mode 100644 index 49abcb50c33..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Utilities/XAJavaTypeScanner.cs +++ /dev/null @@ -1,135 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Java.Interop.Tools.Cecil; -using Microsoft.Android.Build.Tasks; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Mono.Cecil; -using Xamarin.Android.Tools; - -namespace Xamarin.Android.Tasks; - -class XAJavaTypeScanner -{ - // Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must - // be scanned for Java types. - static readonly HashSet SpecialAssemblies = new HashSet (StringComparer.OrdinalIgnoreCase) { - "Java.Interop.dll", - "Mono.Android.dll", - "Mono.Android.Runtime.dll", - }; - - public bool ErrorOnCustomJavaObject { get; set; } - - readonly TaskLoggingHelper log; - readonly TypeDefinitionCache cache; - readonly AndroidTargetArch targetArch; - - public XAJavaTypeScanner (AndroidTargetArch targetArch, TaskLoggingHelper log, TypeDefinitionCache cache) - { - this.targetArch = targetArch; - this.log = log; - this.cache = cache; - } - - public List GetJavaTypes (ICollection inputAssemblies, XAAssemblyResolver resolver) - { - var types = new List (); - var inputItems = inputAssemblies - .Where (a => ShouldScan (a)) - .ToList (); - var monoAndroid = inputItems.FirstOrDefault (a => Path.GetFileName (a.ItemSpec) == "Mono.Android.dll"); - if (monoAndroid != null) { - inputItems.Remove (monoAndroid); - inputItems.Insert (0, monoAndroid); - } - foreach (ITaskItem asmItem in inputItems) { - log.LogDebugMessage ($"[{targetArch}] Scanning assembly '{asmItem.ItemSpec}' for Java types"); - - AndroidTargetArch arch = MonoAndroidHelper.GetTargetArch (asmItem); - if (arch != targetArch) { - throw new InvalidOperationException ($"Internal error: assembly '{asmItem.ItemSpec}' should be in the '{targetArch}' architecture, but is in '{arch}' instead."); - } - - AssemblyDefinition? asmdef = resolver.Load (asmItem.ItemSpec); - if (asmdef == null) { - log.LogDebugMessage ($"[{targetArch}] Unable to load assembly '{asmItem.ItemSpec}'"); - continue; - } - - foreach (ModuleDefinition md in asmdef.Modules) { - foreach (TypeDefinition td in md.Types) { - AddJavaType (td, types); - } - } - } - - return types; - } - - public List GetJavaTypes (AssemblyDefinition assembly) - { - var types = new List (); - - foreach (ModuleDefinition md in assembly.Modules) { - foreach (TypeDefinition td in md.Types) { - AddJavaType (td, types); - } - } - - return types; - } - - bool ShouldScan (ITaskItem assembly) - { - string name = Path.GetFileName (assembly.ItemSpec); - if (SpecialAssemblies.Contains (name)) { - return true; - } - - string? hasMonoAndroidReferenceMetadata = assembly.GetMetadata ("HasMonoAndroidReference"); - if (hasMonoAndroidReferenceMetadata.IsNullOrEmpty ()) { - return true; // Just in case - the metadata missing might be a false negative - } - - if (Boolean.TryParse (hasMonoAndroidReferenceMetadata, out bool hasMonoAndroidReference)) { - return hasMonoAndroidReference; - } - - // A catch-all, it's better to do more work than to miss something important. - return true; - } - - public void AddJavaType (TypeDefinition type, List types) - { - if (type == null) - throw new ArgumentNullException (nameof (type)); - if (types == null) - throw new ArgumentNullException (nameof (types)); - - if (type.HasJavaPeer (cache)) { - // For subclasses of e.g. Android.App.Activity. - types.Add (type); - } else if (type.IsClass && !type.IsSubclassOf ("System.Exception", cache) && type.ImplementsInterface ("Android.Runtime.IJavaObject", cache)) { - string message = $"XA4212: Type `{type.FullName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."; - - if (ErrorOnCustomJavaObject) { - log.LogError (message); - } else { - log.LogWarning (message); - } - return; - } - - if (!type.HasNestedTypes) { - return; - } - - foreach (TypeDefinition nested in type.NestedTypes) { - AddJavaType (nested, types); - } - } -} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index 8f5a9ec54d6..08bb0541f52 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -47,7 +47,6 @@ - diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 6075d981d88..6bd5e77a749 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1377,10 +1377,8 @@ because xbuild doesn't support framework reference assemblies. Inputs="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)" Outputs="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')"> + BeforeTargets="_CheckForInvalidConfigurationAndPlatform;_GenerateJavaStubs;Build;Publish"> From c210e3a9ec43e858a0431a482d83cf6a20452eb3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:08:40 +0200 Subject: [PATCH 45/58] Document trimmable JNI interoperability Describe generated Java wrappers, managed TypeMaps, JNI registration, peer lifetime, and CoreCLR/NativeAOT interop. Link the guide from the documentation index. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/README.md | 1 + .../guides/internals/JavaJNI_Interop.md | 923 +++--------------- 2 files changed, 163 insertions(+), 761 deletions(-) diff --git a/Documentation/README.md b/Documentation/README.md index 826dff3e082..14e2a93b9fa 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -23,6 +23,7 @@ * [`.axml` CodeBehind Support](docs-mobile/features/layout-code-behind/index.md) * [MSBuild Best Practices](guides/MSBuildBestPractices.md) * [D8 and R8 Integration](guides/D8andR8.md) + * [Java and managed interoperability](guides/internals/JavaJNI_Interop.md) * [Profiling Startup](guides/profiling.md) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 2d6e8c82d72..7d06fa1f544 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,766 +1,167 @@ - -- [Introduction](#introduction) -- [Java <-> Managed interoperability overview](#java-managed-interoperability-overview) - - [Java Callable Wrappers (JCW)](#java-callable-wrappers-jcw) -- [Registration](#registration) - - [Dynamic registration](#dynamic-registration) - - [Dynamic Java Callable Wrappers registration code](#dynamic-java-callable-wrappers-registration-code) - - [Dynamic Registration call sequence](#dynamic-registration-call-sequence) - - [Marshal methods](#marshal-methods) - - [Marshal Methods Java Callable Wrappers registration code](#marshal-methods-java-callable-wrappers-registration-code) - - [Marshal methods C# source code](#marshal-methods-c-source-code) - - [JNI requirements](#jni-requirements) - - [LLVM IR code generation](#llvm-ir-code-generation) - - [Assembly rewriting](#assembly-rewriting) - - [Wrappers for methods with non-blittable types](#wrappers-for-methods-with-non-blittable-types) - - [UnmanagedCallersOnly attribute](#unmanagedcallersonly-attribute) - - [Marshal Methods Registration call sequence](#marshal-methods-registration-call-sequence) - - -# Introduction - -At the core of `.NET for Android` is its ability to interoperate with -the Java/Kotlin APIs implemented in the Android system. To make it -work, it is necessary to "bridge" the two separate worlds of Java VM -(`ART` in the Android OS) and the Managed VM (`MonoVM`). Application -developers expect to be able to call native Android APIs and receive -calls (or react to events) from the Android side using code written in -one of the .NET managed languages. To make it work, `.NET for Android` -employs a number of techniques, both at build and at run time, which -are described in the sections below. - -This guide is meant to explain the technical implementation in a way -that is sufficient to understand the system without having to read the -actual source code. - -# Java <-> Managed interoperability overview - -Java VM and Managed VM are two entirely separate entities which -co-exist in the same process/application. Despite sharing the same -process resources, they don't "naturally" communicate with each other. -There is no direct way to call Java/Kotlin from .NET a'la the -`p/invoke` mechanism which allows calling native code APIs. Nor there -exists a way for Java/Kotlin code to invoke managed methods. To make -it possible, `.NET for Android` takes advantage of the Java's [JNI](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html) -(`Java Native Interface`), a mechanism that allows native code -(.NET managed code being "native" in this context) to register -implementations of Java methods, written outside the Java VM and in -languages other than Java/Kotlin (for instance in `C`, `C++` or -`Rust`). - -Such methods need to be appropriately declared in the Java code, for -instance: - -```java -class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Each native method is declared using the `native` keyword, and -whenever it is invoked from other Java code, the Java VM will use the -JNI to invoke the target method. - -Native methods can be registered either dynamically (by calling the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function) or "statically", by providing a native shared library -which exports a symbol with appropriate name which points to the -native function implementing the Java method. - -Both ways of registration are described in detail in the following -sections. - -## Java Callable Wrappers (JCW) - -`.NET for Android` wraps the entire Android API by generating -appropriate C# code which mirrors the Java/Kotlin code (classes, -interfaces, methods, properties etc). Each generated class that -corresponds to a Java/Kotlin type, is derived from the -`Java.Lang.Object` class (implemented in the `Mono.Android` assembly), -which marks it as a "Java interoperable type", meaning that it can -implement or override virtual Java methods. To make registration and -invoking of such methods possible, it is necessary to generate a Java -class which mirrors the Managed one and provides an entry point to -the Java <-> Managed transition. The Java classes are generated -during application (as well as `.NET for Android`) build and we call -them **Java Callable Wrappers** (or **JCW** for short). For instance, -the following managed class: - -```csharp -public class MainActivity : AppCompatActivity -{ - public override Android.Views.View? OnCreateView (Android.Views.View? parent, string name, Android.Content.Context context, Android.Util.IAttributeSet attrs) - { - return base.OnCreateView (parent, name, context, attrs); - } - - protected override void OnCreate (Bundle savedInstanceState) - { - base.OnCreate(savedInstanceState); - DoSomething (savedInstanceState); - } - - void DoSomething (Bundle bundle) - { - // do something with the bundle - } -} -``` - -overrides two Java virtual methods found in the `AppCompatActivity` -type: `OnCreateView` and `OnCreate`. The `DoSomething` method does -not correspond to any method found in the base Java type, and thus it -won't be included in the JCW. - -The Java Callable Wrapper generated for the above class would look as -follows (a few generated methods not relevant to the discussion have -been omitted for brevity): +# Java and managed interoperability (trimmable TypeMap) + +This guide describes the **current** .NET for Android application interop +path for CoreCLR and NativeAOT. `AndroidTypeMapImplementation=trimmable` is the +only supported application TypeMap implementation. The Java VM (ART) and the +managed runtime communicate through JNI; the build supplies generated Java +callable wrappers (JCWs) and managed TypeMap assemblies, while handwritten +Java and managed runtime code connects those generated pieces. This is not a +description of the former MonoVM/native LLVM TypeMap or assembly-rewriting +pipeline. + +## What is generated + +| Piece | Purpose and source | +| --- | --- | +| Bound managed API types | Managed callable wrappers for Android/Java APIs use `JniPeerMembers` to invoke Java methods. These are distinct from the app's Java callable wrappers; see [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) for an invocation. | +| JCW `.java` files | Java classes for managed Java peers, with constructors and `n_*` native callbacks for Java-to-managed calls. [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) produces them from the scanned peer model. | +| TypeMap DLLs | `_{AssemblyName}.TypeMap.dll` files hold mappings, proxy types, and native registration methods; `_Microsoft.Android.TypeMaps.dll` is the root that connects them. [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and [`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs) emit these managed assemblies. | +| Registration and manifest | The task generates `ApplicationRegistration.java` from the scanned peers (not from a Java template); it also writes the ACW map (`acw-map.txt`) and merged `AndroidManifest.xml`. | + +The [scanner](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +reads assembly metadata to discover Java peers and their Java-visible +constructors and methods. The +[`GenerateTrimmableTypeMap` task](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) +drives the scan, validates it, emits the managed and Java outputs, and removes +stale JCW sources. The +[shared MSBuild targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) +run `_GenerateTrimmableTypeMap` after `CoreCompile`; their output lives under +`obj/.../typemap/` (not in handwritten source). Inspect +`typemap/typemap-assemblies.txt` and `typemap/java-files.txt` to see what +that build actually generated. The +[TypeMap build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) +documents target ordering, output stamps, and incremental regeneration. + +The root TypeMap assembly anchors the per-assembly TypeMaps and initializes +the lookup dictionaries. Mapping attributes in those per-assembly DLLs +associate Java names, managed types, and generated proxy types. ACW proxies +implement `IAndroidCallableWrapper` for native registration; other proxies +do not need that interface. These are **managed metadata**, not a lookup +table in a native binary. The runtime +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses the mappings to resolve Java objects and managed peer types. + +## Managed calls Java + +A bound C# method uses its `JniPeerMembers` and a JNI member signature to +call Java through Java.Interop. For example, +`Android.App.Activity.ReportFullyDrawn()` invokes +`_members.InstanceMethods.InvokeVirtualVoidMethod(...)` with +`"reportFullyDrawn.()V"` and the current peer. Static calls use the +corresponding `StaticMethods` APIs. Arguments and return values cross JNI +with the appropriate Java object references and value conversions. This +direction does **not** call a JCW's `n_*` callback: the bound managed API +is the caller, and the Java implementation is the callee. + +When a JNI object arrives in managed code, +[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +resolves its Java type to a managed target/proxy using the generated map; +[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +reuses or constructs a managed peer for its JNI reference. Conversely, the +type manager resolves managed Java-peer types to JNI type signatures when +needed. The map is about **type and peer resolution**; it does not replace +normal Java.Interop method invocation. + +## Java calls managed code + +The JCW is a real Java class generated for a managed peer. Its Java-visible +methods forward to `native n_*` methods, and its constructors use generated +`nctor_*` callbacks. The generator normally emits a per-class static +initializer like: ```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Understanding the connection between Managed methods and their Java -counterparts is required in order to understand the registration -mechanisms described in sections found later in this document. The -[Dynamic registration](#dynamic-registration) section will expand on -this example in order to explain the details of how the Managed type -and its methods are registered with the Java VM. - -# Registration - -Both mechanisms of method registration rely on generation of [Java -Callable Wrappers](#java-callable-wrappers-jcw), with [Dynamic -registration](#dynamic-registration) requiring more code to be -generated so that the registration can be performed at the runtime. - -JCW are generated only for types that derive from -the `Java.Lang.Object` type. Finding such types is the task of the -Java.Interop's [`JavaTypeScanner`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaTypeScanner.cs), -which uses `Mono.Cecil` to read all the assemblies referenced by the -application and its libraries. The returned list of assemblies is -then used by a variety of tasks, JCW being only one -of them. - -After all types are found, -[`JavaCallableWrapperGenerator`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -is invoked in order to analyze each method in each type, looking for -those which override a virtual Java method and, thus, need to be -included in the wrapper class code. The generator optionally (if -[marshal methods](#marshal-methods) are enabled) passes each method to -an implementation of the -[`Java.Interop.Tools.JavaCallableWrappers.JavaCallableMethodClassifier`](../../external/Java.Interop/src/Java.Interop.Tools.JavaCallableWrappers/Java.Interop.Tools.JavaCallableWrappers/JavaCallableWrapperGenerator.cs) -abstract class (which is -[`MarshalMethodsClassifier`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsClassifier.cs) -in our case), to check whether the given method can be registered -statically. - -`JavaCallableWrapperGenerator` looks for methods decorated with the -`[Register]` attribute, which most frequently is created by invoking -its constructor with three parameters: - - 1. Java method name - 2. JNI method signature - 3. "Connector" method name - -The "connector" is a static method which creates a delegate that -subsequently allows calling of the native callback method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -The above code is actually generated in the `Android.App.Activity` -class while .NET for Android is built, from which our example -`MainActivity` eventually derives. - -What happens with the above code depends on the registration mechanism -and is described in the sections below. - -## Dynamic registration - -This registration mechanism has been used by `.NET for Android` since -the beginning and it will remain in use for the foreseeable future -when the application is built in the `Debug` configuration or when -[Marshal Methods](#marshal-methods) are turned off. - -### Dynamic Java Callable Wrappers registration code - -Building on the C# example shown in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ -/** @hide */ - public static final String __md_methods; - static { - __md_methods = - "n_onCreateView:(Landroid/view/View;Ljava/lang/String;Landroid/content/Context;Landroid/util/AttributeSet;)Landroid/view/View;:GetOnCreateView_Landroid_view_View_Ljava_lang_String_Landroid_content_Context_Landroid_util_AttributeSet_Handler\n" + - "n_onCreate:(Landroid/os/Bundle;)V:GetOnCreate_Landroid_os_Bundle_Handler\n" + - ""; - mono.android.Runtime.register ("HelloAndroid.MainActivity, HelloAndroid", MainActivity.class, __md_methods); - } - - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); -} -``` - -Code fragment which takes part in registration is the class's static -constructor. For each method registered for the type (that is, -implemented or overridden in the managed code), the JCW generator -outputs a single string which contains full information about the type -and method to register. Each such registration string is terminated -with the newline character and the entire sequence ends with an empty -string. Together, all the lines are concatenated and placed in the -`__md_methods` static variable. The `mono.android.Runtime.register` -method (see below for more details) is then invoked to register all -the methods. - -### Dynamic Registration call sequence - -All the "native" methods declared in the generated Java type are -registered when the type is constructed or accessed for the first -time. This is when the Java VM invokes the type's static constructor, -kicking off a sequence of calls that eventually ends with all the type -methods registered with JNI: - - 1. `mono.android.Runtime.register` is itself a native method, - declared in the - [`Runtime`](../../src/java-runtime/java/mono/android/Runtime.java) - class of .NET for Android's Java runtime code, and implemented in - the native .NET for Android - [runtime](../../src/monodroid/jni/monodroid-glue.cc) (the - `MonodroidRuntime::Java_mono_android_Runtime_register` method). - Purpose of this method is to prepare a call into the - .NET for Android managed runtime code, the - [`Android.Runtime.JNIEnv::RegisterJniNatives`](../../src/Mono.Android/Android.Runtime/JNIEnv.cs) - method. - 2. `Android.Runtime.JNIEnv::RegisterJniNatives` is passed name of - the managed type for which to register Java methods and uses .NET - reflection to load that type, followed by a call to cache the - type (via `RegisterType` method in the - [`TypeManager`](../../src/Mono.Android/Java.Interop/TypeManager.cs) - class) to end with a call to the - `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - method. - 3. `Android.Runtime.AndroidTypeManager::RegisterNativeMembers` - eventually calls the - `Java.Interop.JniEnvironment.Types::RegisterNatives` method which - first generates a delegate to the native callback method, using - `System.Reflection.Emit` (via the - [`Android.Runtime.JNINativeWrapper::CreateDelegate`](../../src/Mono.Android/Android.Runtime/JNINativeWrapper.cs) - method) and, eventually, invokes Java JNI's `RegisterNatives` - function, finally registering the native methods for a managed - type. - -The `System.Reflection.Emit` sequence mentioned in 3. above is among -the most costly operations, repeated for each registered method. - -Some more information about Java type registration can be found -[here](https://github.com/dotnet/android/wiki/Blueprint#java-type-registration). - -## Marshal methods - -The goal of marshal methods is to completely bypass the [dynamic -registration sequence](#dynamic-registration-call-sequence), replacing -it with native code generated and compiled during application build, -thus saving on the startup time of the application. - -Marshal methods registration mechanism takes advantage of the JNI -ability to look up implementations of `native` Java methods in actual -native (shared) libraries. Such symbols must have names that follow a -set of rules, so that JNI is able to properly locate them (details are -explained in the [JNI Requirements](#jni-requirements) section below). - -To achieve that, the marshal methods mechanism uses a number of -classes which [generate native](#llvm-ir-code-generation) code and -[modify assemblies](#assembly-rewriting) that contain the registered -methods. - -Current implementation of the marshal methods classifier recognizes -the "standard" method registration pattern, using the example of the -`OnCreate` method shown in [Registration](#registration) above. - -The standard pattern consists of: - - * the "connector" method, `GetOnCreate_Landroid_os_Bundle_Handler` - above - * the delegate backing field, `cb_onCreate_Landroid_os_Bundle_` - above - * the native callback method, `n_OnCreate_Landroid_os_Bundle_` above - * and the virtual target method which dispatches the call to the - actual object, `OnCreate` above. - -Whenever the classifier's `ShouldBeDynamicallyRegistered` method is -called, it is passed not only the method's declaring type, but also -the `Register` attribute instance which it then uses to check whether -the method being registered conforms to the "standard" registration -pattern shown above. The connector, native callback methods as well -as the backing field must be private and static in order for the -registered method to be considered as a candidate for static -registration. - -Registered methods which don't follow the "standard" pattern will be -registered dynamically. - -### Marshal Methods Java Callable Wrappers registration code - -Building on the C# example show in the [Java Callable -Wrappers](#java-callable-wrappers-jcw) section, the following Java -code is generated (only the parts relevant to registration are shown): - -```java -public class MainActivity - extends androidx.appcompat.app.AppCompatActivity -{ - public android.view.View onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3) - { - return n_onCreateView (p0, p1, p2, p3); - } - - private native android.view.View n_onCreateView (android.view.View p0, java.lang.String p1, android.content.Context p2, android.util.AttributeSet p3); - - public void onCreate (android.os.Bundle p0) - { - n_onCreate (p0); - } - - private native void n_onCreate (android.os.Bundle p0); +static { + mono.android.Runtime.registerNatives (MyActivity.class); } ``` -Note that, compared to the code generated for the [dynamic -registration](#dynamic-java-callable-wrappers-registration-code) -mechanism, there is no static constructor while the rest -of the code remains exactly the same. - -### Marshal methods C# source code - -The marshal methods sections below will all refer to this code -fragment: - - -```csharp -public class MainActivity : AppCompatActivity -{ - // Connector backing field - static Delegate? cb_onCreate_Landroid_os_Bundle_; - - // Connector method - static Delegate GetOnCreate_Landroid_os_Bundle_Handler () - { - if (cb_onCreate_Landroid_os_Bundle_ == null) - cb_onCreate_Landroid_os_Bundle_ = JNINativeWrapper.CreateDelegate ((_JniMarshal_PPL_V) n_OnCreate_Landroid_os_Bundle_); - return cb_onCreate_Landroid_os_Bundle_; - } - - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -### JNI requirements - -JNI -[specifies](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names) -a number of rules which govern how the native symbol name is -constructed, so that a mapping of object-oriented Java code (with its -package names/namespaces, class names and overloadable methods) into the essentially -"flat" procedural "namespace" of the lowest common denominator C code. - -The precise rules are outlined in the URL above, and their short -version is as follows: - - * Each symbol starts with the `Java_` prefix - * Next follows the mangled (see below) **fully qualified class - name** - * Next the `_` character serves as a separator before - * A mangled **method** name, which is optionally followed by - * Double underscore `__` and the mangled method argument signature - -"Mangling" is a way of encoding certain characters that are not -directly representable both in the source code and in the native -symbol name. The JNI specification allows for direct use of ASCII -letters (capital and lowercase) and digits, while all the other -characters are either represented by placeholders or encoded as 16-bit -hexadecimal Unicode character code (table copied from the JNI -specification for easier reference): - -| Escape sequence | Denotes | -|-----------------|------------------------------------------| -| _0XXXX | a Unicode character XXXX, all lower case | -| _1 | The `_` character | -| _2 | The `;` character in signatures | -| _3 | The `[` character in signatures | -| _ | The `.` or `/` characters | - -Generation of JNI symbol names is performed by the -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -class while generating the native function source code. - -JNI supports two forms of the native symbol name, as signalled in the -bullet list above - a short and a long one. The former is looked up -first by the Java VM, followed by the latter. The latter needs to be -used only for overloaded methods, which is what our generator does. - -### LLVM IR code generation - -[`MarshalMethodsNativeAssemblyGenerator`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs) -uses the LLVM IR generator infrastructure to output both data and -executable code for all the marshal methods wrappers. It is not -necessary to understand the generated code unless one needs to modify -it, so this document only shows the equivalent C++ code which can -serve as a guide to understanding how the marshal method runtime -invocation works: - -```C++ -using get_function_pointer_fn = void(*)(uint32_t mono_image_index, uint32_t class_index, uint32_t method_token, void*& target_ptr); - -static get_function_pointer_fn get_function_pointer; - -void xamarin_app_init (get_function_pointer_fn fn) noexcept -{ - get_function_pointer = fn; -} - -using android_app_activity_on_create_bundle_fn = void (*) (JNIEnv *env, jclass klass, jobject savedInstanceState); -static android_app_activity_on_create_bundle_fn android_app_activity_on_create_bundle = nullptr; - -extern "C" JNIEXPORT void -JNICALL Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2 (JNIEnv *env, jclass klass, jobject savedInstanceState) noexcept -{ - if (android_app_activity_on_create_bundle == nullptr) { - get_function_pointer ( - 16, // mono image index - 0, // class index - 0x0600055B, // method token - reinterpret_cast(android_app_activity_on_create_bundle) // target pointer - ); - } - - android_app_activity_on_create_bundle (env, klass, savedInstanceState); -} -``` - -The `xamarin_app_init` function is output only once and is called by -the `.NET for Android` runtime twice during application startup - once -to pass `get_function_pointer_fn` which does **not** use any locks (as -we know that until a certain point during startup we are in a single -lock, so no data access races can happen) and the other time just -before handing control over to the MonoVM, to pass pointer to -`get_function_pointer_fn` which **does** employ locking (since during -runtime it may very well happen that our generated Java native -functions will be called from different threads simultaneously). - -The `Java_helloandroid_MainActivity_n_1onCreate__Landroid_os_Bundle_2` -function is a template which is repeated for each Java native -function, with each function having its own set of arguments and its -own callback backing field (`android_app_activity_on_create_bundle` -here). - -The `get_function_pointer` function takes as parameters indexes into a -couple of tables, one for `MonoImage*` pointers and the other for -`MonoClass*` pointers - both of which are generated by the -`MarshalMethodsNativeAssemblyGenerator` class at application build -time and allow for very fast lookup during run time. Target methods -are retrieved by their token value, within the specified `MonoImage*` -(essentially a pointer to managed assembly image in memory) and class. - -The method identified in such manner, **must** be decorated in the -managed code with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute (see [below](#unmanagedcallersonly-attribute) for more -details) so that it can be invoked directly, as if it was a native -method itself, with minimal managed marshaling overhead. - -### Assembly rewriting - -Please refer to the [C# code fragment](#marshal-methods-c-source-code) -above in order to understand this section. - -Managed assemblies (including `Mono.Android.dll`) which contain Java -types need to be usable in two contexts: with the "traditional" -dynamic registration and with marshal methods. Both of these -mechanisms, however, have different requirements. We cannot assume -that any assembly (either from .NET for Android or a third party nuget) -will have "marshal methods friendly" code and thus we need to make -sure that the code meets our requirements. - -We do it by reading each relevant assembly and modifying it by -altering the definition of the native callbacks and removing the -code that's no longer used by marshal methods. This task is performed -by the -[`MarshalMethodsAssemblyRewriter`](../../src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs) -invoked during application build after all the assemblies are linked -but **before** type maps are generated (as rewriting **will** alter -the method and potentially type tokens) - -The exact modifications we apply are: - - * Removal of the **connector backing field** - * Removal of the **connector method** - * Generation of a **native callback wrapper** method, which catches - and propagates unhandled exceptions thrown by the native callback - or the target method. This method is decorated with the - `[UnmanagedCallersOnly]` attribute and called directly from the - native code. - * Optionally, generate code in the **native callback wrapper** to handle - [non-blittable types](#wrappers-for-methods-with-non-blittable-types). - -All the modifications are performed with `Mono.Cecil`. - -After modifications, the assembly contains equivalent of the following -C# code for each marshal method: - -```csharp -public class MainActivity : AppCompatActivity -{ - // Native callback - static void n_OnCreate_Landroid_os_Bundle_ (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var savedInstanceState = global::Java.Lang.Object.GetObject (native_savedInstanceState, JniHandleOwnership.DoNotTransfer); - __this.OnCreate (savedInstanceState); - } - - // Native callback exception wrapper - [UnmanagedCallersOnly] - static void n_OnCreate_Landroid_os_Bundle__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState) - { - try { - n_OnCreate_Landroid_os_Bundle_ (jnienv, native__this, native_savedInstanceState) - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - } - } - - // Target method - [Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")] - protected virtual unsafe void OnCreate (Android.OS.Bundle? savedInstanceState) - { - const string __id = "onCreate.(Landroid/os/Bundle;)V"; - try { - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue ((savedInstanceState == null) ? IntPtr.Zero : ((global::Java.Lang.Object) savedInstanceState).Handle); - _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, __args); - } finally { - global::System.GC.KeepAlive (savedInstanceState); - } - } -} -``` - -#### Wrappers for methods with non-blittable types - -The -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute requires that all the argument types as well as the method -return type are -[blittable](https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types). - -Among these types is one that's commonly used by the managed classes -implementing Java methods: `bool`. Currently this is the **only** -non-blittable type we've encountered in bindings, so at this point it -is the only one supported by the assembly rewriter. - -Whenever we encounter a method with a non-blittable type, we must -generate a wrapper for it, so that we can decorate it with the -`[UnmanagedCallersOnly]` attribute. This is easier and less error -prone than modifying the native callback method's IL stream to -implement the necessary conversion. - -An example of such method is -`Android.Views.View.IOnTouchListener::OnTouch`: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} -``` - -As it returns a `bool` value, it needs a wrapper to cast the return -value properly. Each wrapper method retains the native callback method -name, but appends the `_mm_wrapper` suffix to it: - -```csharp -static bool n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_ (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - var e = global::Java.Lang.Object.GetObject (native_e, JniHandleOwnership.DoNotTransfer); - bool __ret = __this.OnTouch (v, e); - return __ret; -} - -[UnmanagedCallersOnly] -static byte n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent__mm_wrapper (IntPtr jnienv, IntPtr native__this, IntPtr native_v, IntPtr native_e) -{ - try { - return n_OnTouch_Landroid_view_View_Landroid_view_MotionEvent_(jnienv, native__this, native_v, native_e) ? 1 : 0; - } catch (Exception ex) { - Android.Runtime.AndroidEnvironmentInternal.UnhandledException (ex); - return default; - } -} -``` - -The wrapper's return statement uses the ternary operator to "cast" the -boolean value to `1` (for `true`) or `0` (for `false`) because the -value of `bool` across the managed runtime can take a range of values: - - * `0` for `false` - * `-1` or `1` for `true` - * `!= 0` for true - -Since the `bool` type in C# can be 1, 2 or 4 bytes long, we need to -cast it to some type of a known and static size. The managed type -`byte` was chosen as it corresponds to the Java/JNI `jboolean` type, -defined as an unsigned 8-bit type. - -Whenever an **argument** value needs to be converted between `byte` and -`bool`, we generate code that is equivalent of the `argument != 0` -comparison, for instance for the -`Android.Views.View.IOnFocusChangeListener::OnFocusChange` method: - -```csharp -[UnmanagedCallersOnly] -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, byte hasFocus) -{ - n_OnFocusChange_Landroid_view_View_Z (jnienv, native__this, native_v, hasFocus != 0); -} - -static void n_OnFocusChange_Landroid_view_View_Z (IntPtr jnienv, IntPtr native__this, IntPtr native_v, bool hasFocus) -{ - var __this = global::Java.Lang.Object.GetObject (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; - var v = global::Java.Lang.Object.GetObject (native_v, JniHandleOwnership.DoNotTransfer); - __this.OnFocusChange (v, hasFocus); -} -``` - -#### UnmanagedCallersOnly attribute - -Each marshal methods native callback method is decorated with the -[`[UnmanagedCallersOnly]`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0) -attribute, in order for us to be able to invoke the callback directly -from native code with minimal overhead compared to traditional -managed-from-native method calls (`mono_runtime_invoke`) - -### Marshal Methods Registration call sequence - -The sequence described in the [dynamic -registration sequence](#dynamic-registration-call-sequence) section -above is completely removed for the marshal methods. What remains -common for both dynamic and marshal methods registration, is the -resolution of the native function target done by the Java VM runtime. -In both cases the method declared in a Java class as `native` is -looked up by the Java VM when first JIT-ing the code. The difference -lies in the way this lookup is performed. - -Dynamic registration uses the -[`RegisterNatives`](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives) -JNI function at the runtime, which stores a pointer to the registered -method inside the structure which describes a Java class in the Java -VM. - -Marshal methods, however, don't register anything with the JNI, -instead they rely on the symbol lookup mechanism of the Java VM. -Whenever a call to `native` Java method is JIT-ed and it is not -registered previously using the `RegisterNatives` JNI function, Java -VM will proceed to look for symbols in the process runtime image (e.g. -using `dlopen` + `dlsym` calls on Unix) and, having found a matching -symbol, use pointer to it as the target of the `native` Java method -call. +This is a representative *generated* shape, not a source file to edit. +Application and instrumentation wrappers defer registration through a +generated guarded `__md_registerNatives()` method where static initialization +would be too early. The `registerNatives` declaration itself is **handwritten** +in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java). + +At startup, +[`JNIEnvInit`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs) +installs the trimmable type and value managers and calls +`TrimmableTypeMap.RegisterNativeMethods()`. That binds +`mono/android/Runtime.registerNatives` to a managed callback using JNI +`RegisterNatives`. When a generated JCW requests registration for its +class, `TrimmableTypeMap.OnRegisterNatives` looks up that Java class in the +generated map and asks its generated `IAndroidCallableWrapper` registration +code to bind the class's `n_*` methods. ART can then call those registered +methods, which transition through generated managed JNI-ABI entrypoints to +the managed implementation. + +The [generator model](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) +records method name, JNI signature, and callback target for each native +registration. For exported or directly callable managed methods, +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) +emits an `UnmanagedCallersOnly` entrypoint that converts JNI arguments, calls +the managed method, converts its result, and handles exceptions. Other +callbacks use the generated registration's corresponding marshal target. +Do not confuse these **generated managed** entrypoints with the handwritten +Java declarations or the old native registration table. + +## Startup and peer lifetime + +CoreCLR's [native host](../../../src/native/clr/host/host.cc) captures the +Java VM at `JNI_OnLoad`, starts the managed runtime from +`Runtime.initInternal`, and enters `JNIEnvInit.Initialize`. The native +`Runtime.register` entrypoint remains a compatibility stub; TypeMap-based +native registration is handled on the managed side. + +NativeAOT instead enters +[`JavaInteropRuntime.JNI_OnLoad` / `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs), +constructs a +[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), +and calls `JNIEnvInit.InitializeNativeAotRuntime`. Both runtime paths +register the same `TrimmableTypeMap` JNI callback, but NativeAOT cannot +depend on CoreCLR's reflection-based fallback for missing mappings. +[`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) +provides the generated proxy's AOT-safe peer creation hook. + +A managed `Java.Lang.Object` and its Java object are peers, not one object +shared by two garbage collectors. Generated JCWs implement the Java-side +[`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) +reference hooks; the runtime's +[`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) +and `TrimmableTypeMapValueManager` register, find, replace, and finalize +managed peers while respecting JNI reference ownership. Disposing a managed +peer or letting collection retire a pair can remove the association; keeping +a raw JNI handle after that does not keep the old managed instance alive. + +## Trimming, AOT, and diagnosing failures + +The [CoreCLR targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) +feed the generated TypeMap assemblies to ILLink as trimmable inputs. With +`PublishTrimmed=true`, they regenerate JCWs into `typemap/linked-java/` from +the **linked** assemblies, so compiled Java matches the surviving managed +callbacks. Check `typemap/linked-java-files.txt` rather than only the +pre-trim `java-files.txt` in such a build. The +[NativeAOT targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) +feed TypeMaps to ILC and use its output plus the ACW map for Java shrinker +configuration. Both pipelines depend on reachable managed types and their +generated registrations; a method or type missing after trimming is not +fixed by restoring a historical native TypeMap path. + +When investigating: + +1. Check the build's generated TypeMap DLL list, JCW list, `acw-map.txt`, + and compiled Java sources (post-trim `linked-java/` if applicable). + Compare the actual Java class/name/signature with the generated native + registration and its managed target. +2. Check `adb logcat` for a Java `UnsatisfiedLinkError`, JNI registration + failure, or managed exception during activation; distinguish a missing + callback from a missing managed peer mapping. Inspect the runtime's + `TrimmableTypeMap` and generated proxy paths for the latter. +3. For invalid or prematurely disposed JNI object references, use + [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). For build + errors about unsupported `AndroidTypeMapImplementation` values, see + [the build property](../../docs-mobile/building-apps/build-properties.md); + duplicate Java-visible constructor signatures are reported as + [XA4259](../../docs-mobile/messages/xa4259.md). + +The [trimmable TypeMap build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) +cover generation, trimming, NativeAOT and CoreCLR packaging, and incremental +Java output. Start there when changing generated interop behavior. From 72823ec9a05601ad60aad685cfc0a680bd56be1b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:25:04 +0200 Subject: [PATCH 46/58] Expand trimmable JNI interop guide with generated code paths Explain the former registration concepts using current TypeMap and JCW examples, managed UTF-8 JNI registration, ABI marshalling, startup, peer lifecycle, and trimming. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../guides/internals/JavaJNI_Interop.md | 607 +++++++++++++----- 1 file changed, 461 insertions(+), 146 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 7d06fa1f544..542a9c09078 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,167 +1,482 @@ -# Java and managed interoperability (trimmable TypeMap) +# Java and managed interoperability with the trimmable TypeMap -This guide describes the **current** .NET for Android application interop -path for CoreCLR and NativeAOT. `AndroidTypeMapImplementation=trimmable` is the -only supported application TypeMap implementation. The Java VM (ART) and the -managed runtime communicate through JNI; the build supplies generated Java -callable wrappers (JCWs) and managed TypeMap assemblies, while handwritten -Java and managed runtime code connects those generated pieces. This is not a -description of the former MonoVM/native LLVM TypeMap or assembly-rewriting -pipeline. +This is an internal guide to the interop path used by **CoreCLR and NativeAOT +Android applications**. The Java VM (ART) and the managed runtime are separate +environments in one process. JNI is the boundary in both directions: bound C# +types call Java methods through Java.Interop, and Java calls managed methods +through generated Java callable wrappers (JCWs) with registered JNI entrypoints. -## What is generated +`AndroidTypeMapImplementation=trimmable` is the only supported application +TypeMap implementation. Earlier versions of this guide described a choice +between reflection/delegate-based "dynamic" registration and native +LLVM-generated marshal methods. Neither describes this path. Here, the build +generates **managed** TypeMap assemblies (including proxy and marshal code) +and Java sources; the managed runtime registers their native callbacks with +ART. There is no application-level native TypeMap lookup table, generated +`Java_...` symbol for each callback, or marshal-method callback rewriting. -| Piece | Purpose and source | +| Earlier guide | Trimmable TypeMap path | | --- | --- | -| Bound managed API types | Managed callable wrappers for Android/Java APIs use `JniPeerMembers` to invoke Java methods. These are distinct from the app's Java callable wrappers; see [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) for an invocation. | -| JCW `.java` files | Java classes for managed Java peers, with constructors and `n_*` native callbacks for Java-to-managed calls. [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) produces them from the scanned peer model. | -| TypeMap DLLs | `_{AssemblyName}.TypeMap.dll` files hold mappings, proxy types, and native registration methods; `_Microsoft.Android.TypeMaps.dll` is the root that connects them. [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and [`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs) emit these managed assemblies. | -| Registration and manifest | The task generates `ApplicationRegistration.java` from the scanned peers (not from a Java template); it also writes the ACW map (`acw-map.txt`) and merged `AndroidManifest.xml`. | - -The [scanner](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -reads assembly metadata to discover Java peers and their Java-visible -constructors and methods. The -[`GenerateTrimmableTypeMap` task](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) -drives the scan, validates it, emits the managed and Java outputs, and removes -stale JCW sources. The -[shared MSBuild targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) -run `_GenerateTrimmableTypeMap` after `CoreCompile`; their output lives under -`obj/.../typemap/` (not in handwritten source). Inspect -`typemap/typemap-assemblies.txt` and `typemap/java-files.txt` to see what -that build actually generated. The -[TypeMap build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) -documents target ordering, output stamps, and incremental regeneration. - -The root TypeMap assembly anchors the per-assembly TypeMaps and initializes -the lookup dictionaries. Mapping attributes in those per-assembly DLLs -associate Java names, managed types, and generated proxy types. ACW proxies -implement `IAndroidCallableWrapper` for native registration; other proxies -do not need that interface. These are **managed metadata**, not a lookup -table in a native binary. The runtime -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses the mappings to resolve Java objects and managed peer types. +| JCW static initializer built `__md_methods` and called `Runtime.register` with an assembly-qualified type name. | A generated JCW calls `Runtime.registerNatives(Class)`; the generated managed proxy supplies the registrations for that Java class. | +| "Dynamic" registration used connector delegates and reflection/`Reflection.Emit` at runtime. | The scanner reads connector metadata to locate existing callbacks, but generated `UnmanagedCallersOnly` forwarders register their function pointers without making per-method delegates. | +| "Marshal methods" relied on JNI `Java_...` native exports, native LLVM code, and a callback assembly rewriter. | The TypeMap emitter writes managed proxy/entrypoint IL; `RegisterNatives` binds method name/signature pairs to those entrypoints. | +| JNI symbol mangling selected the native implementation; `_mm_wrapper` adapted booleans and exceptions. | The generated registration table uses NUL-terminated UTF-8 names/signatures; generated managed entrypoints perform JNI ABI conversion and exception handoff. | -## Managed calls Java +## Terms and a running example -A bound C# method uses its `JniPeerMembers` and a JNI member signature to -call Java through Java.Interop. For example, -`Android.App.Activity.ReportFullyDrawn()` invokes -`_members.InstanceMethods.InvokeVirtualVoidMethod(...)` with -`"reportFullyDrawn.()V"` and the current peer. Static calls use the -corresponding `StaticMethods` APIs. Arguments and return values cross JNI -with the appropriate Java object references and value conversions. This -direction does **not** call a JCW's `n_*` callback: the bound managed API -is the caller, and the Java implementation is the callee. +| Term | Meaning | +| --- | --- | +| Bound managed type (MCW) | A C# binding for an existing Java class, such as `Android.App.Activity`. Its `JniPeerMembers` invokes Java methods. | +| Java callable wrapper (JCW, or ACW) | A *generated Java class* for a managed subclass or Java-visible implementation. Java overrides call `native n_*` methods, which lead back to managed code. | +| TypeMap assembly | A *generated managed DLL* containing Java-name/managed-type associations, proxy types, and (for JCWs) registration and JNI entrypoint code. | +| Java peer / managed peer | Two objects representing one interop instance, with separate runtimes and garbage collectors. JNI references and peer bookkeeping link their lifetimes. | -When a JNI object arrives in managed code, -[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -resolves its Java type to a managed target/proxy using the generated map; -[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -reuses or constructs a managed peer for its JNI reference. Conversely, the -type manager resolves managed Java-peer types to JNI type signatures when -needed. The map is about **type and peer resolution**; it does not replace -normal Java.Interop method invocation. +For example, an application can subclass a bound `Activity`: + +```csharp +using Android.App; +using Android.OS; -## Java calls managed code +namespace MyApp +{ + [Activity (Name = "my.app.MainActivity", MainLauncher = true)] + public class MainActivity : Activity + { + protected override void OnCreate (Bundle? savedInstanceState) + { + base.OnCreate (savedInstanceState); + // Application work here. + } + } +} +``` + +The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI +signature `(Landroid/os/Bundle;)V`. The app override need not repeat +`[Register]`: the scanner follows the registered base method through the +override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) +include both an explicitly registered `MainActivity` and an unannotated +`UserActivity` override. -The JCW is a real Java class generated for a managed peer. Its Java-visible -methods forward to `native n_*` methods, and its constructors use generated -`nctor_*` callbacks. The generator normally emits a per-class static -initializer like: +The corresponding JCW is roughly the following Java. This excerpt omits +constructor and peer-reference code; its class name, registration call, +override, and native callback name match the +[JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): ```java -static { - mono.android.Runtime.registerNatives (MyActivity.class); +package my.app; + +public class MainActivity extends android.app.Activity + implements mono.android.IGCUserPeer +{ + static { + mono.android.Runtime.registerNatives (MainActivity.class); + } + + @Override + public void onCreate (android.os.Bundle p0) + { + n_OnCreate_Landroid_os_Bundle_ (p0); + } + + public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0); } ``` -This is a representative *generated* shape, not a source file to edit. -Application and instrumentation wrappers defer registration through a -generated guarded `__md_registerNatives()` method where static initialization -would be too early. The `registerNatives` declaration itself is **handwritten** -in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java). - -At startup, -[`JNIEnvInit`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs) -installs the trimmable type and value managers and calls -`TrimmableTypeMap.RegisterNativeMethods()`. That binds -`mono/android/Runtime.registerNatives` to a managed callback using JNI -`RegisterNatives`. When a generated JCW requests registration for its -class, `TrimmableTypeMap.OnRegisterNatives` looks up that Java class in the -generated map and asks its generated `IAndroidCallableWrapper` registration -code to bind the class's `n_*` methods. ART can then call those registered -methods, which transition through generated managed JNI-ABI entrypoints to -the managed implementation. - -The [generator model](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) -records method name, JNI signature, and callback target for each native -registration. For exported or directly callable managed methods, -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) -emits an `UnmanagedCallersOnly` entrypoint that converts JNI arguments, calls -the managed method, converts its result, and handles exceptions. Other -callbacks use the generated registration's corresponding marshal target. -Do not confuse these **generated managed** entrypoints with the handwritten -Java declarations or the old native registration table. - -## Startup and peer lifetime - -CoreCLR's [native host](../../../src/native/clr/host/host.cc) captures the -Java VM at `JNI_OnLoad`, starts the managed runtime from -`Runtime.initInternal`, and enters `JNIEnvInit.Initialize`. The native -`Runtime.register` entrypoint remains a compatibility stub; TypeMap-based -native registration is handled on the managed side. - -NativeAOT instead enters -[`JavaInteropRuntime.JNI_OnLoad` / `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs), -constructs a -[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), -and calls `JNIEnvInit.InitializeNativeAotRuntime`. Both runtime paths -register the same `TrimmableTypeMap` JNI callback, but NativeAOT cannot -depend on CoreCLR's reflection-based fallback for missing mappings. +The Java name is `my/app/MainActivity`, not the assembly-qualified C# type +name. The method descriptor means one `android.os.Bundle` argument and a +`void` return; the `L...;` denotes an object reference. The generated +callback name is derived from binding metadata. `OnCreate` can call +`base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, +not a second JCW registration. + +## Build-time representation + +[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) +invokes the +[`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +on the application, references, and SDK/framework inputs. The scanner reads +managed assembly metadata, including Java type names, `[Register]` methods, +overrides of registered base/interface methods, constructors, `[Export]` +members, and manifest components. The +[`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) +validates the resulting model and feeds both the Java and managed emitters. +The app's generated JCWs do not come from the old +`JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. + +The main outputs are: + +| Output | Role | +| --- | --- | +| `typemap/java/**/*.java` | Generated JCWs; [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) emits class declarations, constructors, overrides, native declarations, registration, and `IGCUserPeer` hooks. | +| `typemap/_*.TypeMap.dll` | Per-input-assembly managed mapping attributes, proxy types, and JNI registration methods emitted by [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs). | +| `typemap/_Microsoft.Android.TypeMaps.dll` | Root assembly anchoring the per-assembly TypeMaps; its generated `TypeMapLoader.Initialize()` wires the mapping dictionaries ([`RootTypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs)). | +| `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | +| `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | + +Each per-assembly TypeMap contains assembly-level mapping attributes: a key +such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the +managed target type. The root references the per-assembly maps via +`TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, +not `.cs` files; the C# in the sections below is an explanatory translation +of emitted code, not a file that a developer should edit. See +[`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), +[`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), +and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). + +For the example Activity, think of a mapping like this **conceptual C#**: + +```csharp +// Assembly metadata, emitted as IL rather than source: +[assembly: TypeMapAttribute ("my/app/MainActivity", + typeof (MainActivityProxy), typeof (MyApp.MainActivity))] + +// The emitted ACW proxy derives from JavaPeerProxy and implements +// IAndroidCallableWrapper. Proxy names are chosen by the emitter. +``` + +There is no source-level `TypeMap` attribute for application code to write. +The real emitted `TypeMapAttribute` takes the Java key and proxy reference; +its trimmable three-argument form also takes the target reference. An +unconditional mapping has just the key and proxy reference, whereas the +target reference on a conditional mapping lets the trimming pipeline +discard entries for unused managed types. ACW proxies implement +`IAndroidCallableWrapper` to register callbacks; other proxies need not. [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -provides the generated proxy's AOT-safe peer creation hook. +provides peer creation without `Activator.CreateInstance`. + +The +[shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) +run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, +and feed TypeMap DLLs to later steps. They track generated files and remove +stale JCWs when the managed model changes. The target's stamp and output lists +matter on incremental builds: a stale compiled Java class can otherwise +outlive its managed callback. + +CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) +give the TypeMap DLLs to ILLink as trimmable inputs, with +`_Microsoft.Android.TypeMaps` as the entry assembly. For +`PublishTrimmed=true`, a **second scan of linked assemblies** regenerates +JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the +pre-trim Java set is not necessarily the packaged set. NativeAOT's +[targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) +feed the generated DLLs to ILC and use its graph plus the ACW map when +producing Java shrinker configuration. Both modes need reachable managed +mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. + +## Managed code calls Java + +Android bindings are managed wrappers for *existing* Java APIs. For example, +[`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) +uses its bound member descriptor `"reportFullyDrawn.()V"` and calls +`_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The +essential C# shape (simplified) is: + +```csharp +const string id = "reportFullyDrawn.()V"; +_members.InstanceMethods.InvokeVirtualVoidMethod (id, this, null); +``` + +Java.Interop resolves the class and method, invokes JNI with the peer's +reference, and converts values/references according to the binding. Other +bound methods use `JniArgumentValue` arrays and the appropriate virtual, +nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a +per-call method dispatch table: it is involved when Java classes and managed +peers must be mapped or created, not in place of this invocation. + +When JNI returns an object or Java passes one into managed code, +[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +uses the Java name and generated mappings to select a managed target and +proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +looks for an existing managed peer or constructs one from the JNI handle. +In the reverse direction, the type manager can obtain a JNI type signature +for a managed peer type. Do not confuse a bound `Activity` MCW with the +`MainActivity` JCW that implements Java-to-managed overrides. + +## Java code calls managed code + +### JCW declarations and per-class registration + +The generated Java `onCreate` calls its native +`n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated +`nctor_*` native callbacks, and instantiate/activate the managed peer when +appropriate. The class's static block calls +`mono.android.Runtime.registerNatives (MainActivity.class)` when the class +is first initialized. This is **per Java class**. Application and +instrumentation JCWs instead use a guarded `__md_registerNatives()` helper +called from their generated wrappers: registration in their static +initializer can be too early in startup. The declaration of +`registerNatives(Class)` in +[`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) +is handwritten Java; individual JCWs and their registration calls are +generated. + +There is no concatenated `__md_methods` string and no +`Runtime.register("managed.Type, Assembly", ...)` registration in this path. +The old JNI native-symbol mangling rules (`Java_package_Class_method...`) +matter for symbol-lookup-based JNI code, but generated app JCW callbacks +here are associated with their Java declarations through +[`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): +the Java method **name** and **signature** must match; no `Java_...` export +is required for each method. + +### Runtime startup and registration handshake -A managed `Java.Lang.Object` and its Java object are peers, not one object -shared by two garbage collectors. Generated JCWs implement the Java-side +On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures +the VM at `JNI_OnLoad`, initializes the managed runtime from +`Runtime.initInternal`, and enters +[`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). +That installs `TrimmableTypeMapTypeManager` and +`TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls +`TrimmableTypeMap.RegisterNativeMethods()`. The old native +`Runtime.register` entrypoint remains a compatibility stub, **not** the +mechanism that registers every JCW. + +NativeAOT has its own entry: +[`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) +initialize the native host and a +[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), +then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers +the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional +reflection-based peer construction path when a generated mapping is absent. +Its generated `ApplicationRegistration.registerApplications()` calls +`Runtime.registerNatives` for deferred startup types; the +[`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) +invokes it after `JavaInteropRuntime.init(...)`. + +The first native binding is for `mono.android.Runtime.registerNatives` +itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses actual C# UTF-8 string literals for its JNI name and signature: + +```csharp +fixed (byte* name = "registerNatives"u8, + sig = "(Ljava/lang/Class;)V"u8) { + var method = new JniNativeMethod (name, sig, onRegisterNatives); + JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); +} +``` + +When a JCW's static block (or deferred helper) calls this Java native +method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI +name, finds its mapping/proxy, and calls the proxy's +`IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the +class's `n_*` and `nctor_*` methods with ART. The handwritten +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +does the lookup and handoff; the *per-class* method table is in a +**generated managed TypeMap proxy**. The CoreCLR host does not supply a +table of exported native functions for these app methods. + +### Generated registration in C# terms, including UTF-8 + +The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also +implements `IAndroidCallableWrapper`. For each native method it emits a +`JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI +signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` +method stack-allocates the entries and passes a `ReadOnlySpan` +to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and +callback identifiers are illustrative, and the emitter writes IL): + +```csharp +public unsafe void RegisterNatives (JniType jniType) +{ + JniNativeMethod* entries = stackalloc JniNativeMethod [1]; + delegate* unmanaged callback = &OnCreateEntry; + entries [0] = new JniNativeMethod ( + &MethodNameUtf8, &MethodSignatureUtf8, (IntPtr) callback); + JniEnvironment.Types.RegisterNatives ( + jniType.PeerReference, + new ReadOnlySpan (entries, 1)); +} +``` + +The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) +emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in +RVA data**. It can reuse identical signature data. A representative +registration pair for the Activity example is +`n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these +identify the *native* Java declaration, not the C# method's +`GetOnCreate...Handler` connector. There is no runtime decoding of +`__md_methods` or per-callback `Java_...` export lookup. The handwritten +`registerNatives` binding above likewise supplies `byte*` name/signature +data, via C# `u8` literals. + +These name/signature bytes are not Java `String` argument marshalling. JNI's +`GetStringUTFChars`/`NewStringUTF` APIs use +[modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); +the emitter stores the registration identifiers/descriptors as UTF-8 +with explicit NUL terminators (the ASCII names/signatures above have +the same bytes in either encoding). For example, the CoreCLR +[class-name lookup](../../../src/native/clr/host/host-shared.cc) +obtains a Java class name via `GetStringUTFChars`, then converts its dots to +slashes for a TypeMap key. Separately, +[`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) +encodes method names/signatures as UTF-8 with an explicit trailing NUL +for native remapping lookup. Neither path is a substitute for marshalling +Java `String` values across JNI. + +### From JNI ABI to a C# method + +JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then +the declared arguments. Java object handles appear as pointer-sized +parameters; the generated managed entrypoints have JNI-ABI-compatible +signatures and are marked `UnmanagedCallersOnly`. For the example callback, +the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and +`IntPtr savedInstanceState`. This does **not** mean the app override accepts +`IntPtr`: its bound callback/proxy converts the `Bundle` reference and +dispatches the virtual C# `OnCreate(Bundle?)`. + +The scanner reads `[Register ("onCreate", +"(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` +on the bound method (or its base). The third string is **connector metadata**, +not the Java name of the native callback. For a normal registered binding, +the scanner uses it to find the callback's declaring type and name (such as +`n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR +signature. The +[`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +emits an `UnmanagedCallersOnly` forwarder to that existing callback, which +then calls the virtual managed method. The old path made a delegate using +`GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the +trimmable path derives callback metadata from the connector without +creating a delegate per registration. + +`[Export]` (and methods classified as directly callable) take a different +branch. The +[`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) +marks them for a generated direct dispatcher. The +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) +emits the JNI entrypoint and argument conversions, calls the managed target, +converts the return value, and copies back applicable array outputs; it +does not invoke a connector callback. For example, a Java-visible C# +method can be written as: + +```csharp +[Java.Interop.Export ("handleClick")] +public bool HandleClick (Android.Views.View view, int action) +{ + return action != 0; +} +``` + +This is the shape exercised by the +[export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). +The Java wrapper has a `native` declaration with its calculated JNI +signature; the generated managed dispatcher, not the developer's C# +method directly, is the unmanaged entrypoint. + +JNI signatures are **not** CLR method signatures. JNI `Z` is an +eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and +`Landroid/os/Bundle;` a Java object reference. The emitted entrypoint +uses JNI ABI types (a `byte` for a boolean); conversions such as +`jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# +`bool`. For normal registered callbacks the generator uses the +captured original callback signature where available: older bindings +may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. +It must not assume that the callback signature is identical to the +Java descriptor; see the scanner's +[callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) +and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). +No assembly rewriter is needed to insert separate `_mm_wrapper` methods. + +Both generated forwarders and direct dispatchers use a marshal-method +frame: `BeginMarshalMethod`, a `try` around the managed call, exception +handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in +`finally`. An arbitrary managed exception is not simply allowed to +escape across an unmanaged JNI boundary. Compare +[`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) +with the +[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). + +The call sequence for a Java `onCreate(Bundle)` is therefore: + +1. ART enters the generated `MainActivity.onCreate`. +2. That method calls its registered `n_OnCreate_Landroid_os_Bundle_`. +3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the + registered name/signature. For a bound override, it forwards to the + connector-derived bound callback; for a direct export it marshals and + invokes the target itself. +4. Peer lookup/activation and argument conversion supply the managed + `MainActivity` and `Bundle`; virtual dispatch runs the app override. + A call to `base.OnCreate` then goes **managed-to-Java** through + `JniPeerMembers`. + +## Peer activation and lifetime + +A Java-side constructor and a managed-side constructor do not allocate +one shared object. Generated JCW constructors call `super(...)` and, +when the actual Java class matches the wrapper, a generated +`nctor_*` callback to activate the matching managed peer. This class +check matters when a JCW is further subclassed. For some startup +types registration is deferred as described above. The generator +produces the appropriate constructor shape from its scan; compare +[`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) +and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). + +On the managed side, +[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) +uses generated mapping attributes to resolve a Java class and target +type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) +can create the managed object from a handle without late-bound +`Activator.CreateInstance`; activation also accounts for an existing +replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) +resolves names/types, while +[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) +checks for registered peers and handles construction/ownership. + +Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) -reference hooks; the runtime's +hooks `monodroidAddReference` and `monodroidClearReferences`. Managed [`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) -and `TrimmableTypeMapValueManager` register, find, replace, and finalize -managed peers while respecting JNI reference ownership. Disposing a managed -peer or letting collection retire a pair can remove the association; keeping -a raw JNI handle after that does not keep the old managed instance alive. - -## Trimming, AOT, and diagnosing failures - -The [CoreCLR targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) -feed the generated TypeMap assemblies to ILLink as trimmable inputs. With -`PublishTrimmed=true`, they regenerate JCWs into `typemap/linked-java/` from -the **linked** assemblies, so compiled Java matches the surviving managed -callbacks. Check `typemap/linked-java-files.txt` rather than only the -pre-trim `java-files.txt` in such a build. The -[NativeAOT targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) -feed TypeMaps to ILC and use its output plus the ACW map for Java shrinker -configuration. Both pipelines depend on reachable managed types and their -generated registrations; a method or type missing after trimming is not -fixed by restoring a historical native TypeMap path. - -When investigating: - -1. Check the build's generated TypeMap DLL list, JCW list, `acw-map.txt`, - and compiled Java sources (post-trim `linked-java/` if applicable). - Compare the actual Java class/name/signature with the generated native - registration and its managed target. -2. Check `adb logcat` for a Java `UnsatisfiedLinkError`, JNI registration - failure, or managed exception during activation; distinguish a missing - callback from a missing managed peer mapping. Inspect the runtime's - `TrimmableTypeMap` and generated proxy paths for the latter. -3. For invalid or prematurely disposed JNI object references, use - [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). For build - errors about unsupported `AndroidTypeMapImplementation` values, see +tracks add/peek/remove/finalize operations, and the GC bridge coordinates +the two collectors. Local and global JNI references have different +lifetimes: a managed wrapper may need a global reference to keep its +peer accessible after a JNI call returns. `JniHandleOwnership` records +whether an incoming reference is transferred. Calling `Dispose()` can +remove the association; a raw `IntPtr` retained afterward is not a +safe substitute for a live managed peer. For invalid reference +diagnostics, see +[Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). + +## Diagnosing a broken interop path + +Start at the boundary where the failure occurs; these files distinguish +build output, registration, and peer activation: + +1. **Missing or wrong Java class:** inspect the merged manifest, + `typemap/java/` (or CoreCLR's `typemap/linked-java/` after + trimming), `java-files.txt` / `linked-java-files.txt`, and + `acw-map.txt`. Confirm the expected Java name and base/override + signatures. A stale `.class` is not proof that a corresponding + TypeMap entry survived. +2. **Unbound native method / `UnsatisfiedLinkError`:** compare the + generated JCW's `native` method **name and JNI descriptor** with + its generated proxy registration. Check whether + `mono.android.Runtime.registerNatives` ran for the class (or its + deferred helper did), and inspect `adb logcat` for the registration + exception. Do not search for a missing `Java_...` export in this + pipeline. +3. **Wrong managed type or activation failure:** check + `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` + assembly, `TrimmableTypeMap` lookup, proxy construction, and + `JavaMarshalRegisteredPeers`. Verify whether a Java callback came + from a managed-created peer or needs activation from a Java-created + peer. On NativeAOT a missing generated mapping cannot be rescued + by CoreCLR-only reflection. +4. **Release/trim-only failure:** inspect the *linked* managed + assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and + shrinker rules. The pre-trim generated Java is not necessarily + packaged Java. The + [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) + explains target stamps and stale-file handling. +5. **JNI reference or marshal failure:** use `adb logcat` and + [the JNI reference guide](debug-jni-objrefs.md) for ownership and + collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); - duplicate Java-visible constructor signatures are reported as + duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). -The [trimmable TypeMap build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) -cover generation, trimming, NativeAOT and CoreCLR packaging, and incremental -Java output. Start there when changing generated interop behavior. +The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), +[JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), +and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) +exercise these paths. Compare their expected names/signatures against +the generated DLL and Java files before changing runtime registration. From 7b6e8c40398383456014910c45ce3f92d630bb48 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:29:37 +0200 Subject: [PATCH 47/58] Let editors wrap JNI interop guide prose Keep Markdown paragraphs on one source line while preserving list, table, heading, and fenced code structure. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../guides/internals/JavaJNI_Interop.md | 385 +++--------------- 1 file changed, 56 insertions(+), 329 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 542a9c09078..10d9b25ef4d 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -1,19 +1,8 @@ # Java and managed interoperability with the trimmable TypeMap -This is an internal guide to the interop path used by **CoreCLR and NativeAOT -Android applications**. The Java VM (ART) and the managed runtime are separate -environments in one process. JNI is the boundary in both directions: bound C# -types call Java methods through Java.Interop, and Java calls managed methods -through generated Java callable wrappers (JCWs) with registered JNI entrypoints. - -`AndroidTypeMapImplementation=trimmable` is the only supported application -TypeMap implementation. Earlier versions of this guide described a choice -between reflection/delegate-based "dynamic" registration and native -LLVM-generated marshal methods. Neither describes this path. Here, the build -generates **managed** TypeMap assemblies (including proxy and marshal code) -and Java sources; the managed runtime registers their native callbacks with -ART. There is no application-level native TypeMap lookup table, generated -`Java_...` symbol for each callback, or marshal-method callback rewriting. +This is an internal guide to the interop path used by **CoreCLR and NativeAOT Android applications**. The Java VM (ART) and the managed runtime are separate environments in one process. JNI is the boundary in both directions: bound C# types call Java methods through Java.Interop, and Java calls managed methods through generated Java callable wrappers (JCWs) with registered JNI entrypoints. + +`AndroidTypeMapImplementation=trimmable` is the only supported application TypeMap implementation. Earlier versions of this guide described a choice between reflection/delegate-based "dynamic" registration and native LLVM-generated marshal methods. Neither describes this path. Here, the build generates **managed** TypeMap assemblies (including proxy and marshal code) and Java sources; the managed runtime registers their native callbacks with ART. There is no application-level native TypeMap lookup table, generated `Java_...` symbol for each callback, or marshal-method callback rewriting. | Earlier guide | Trimmable TypeMap path | | --- | --- | @@ -51,17 +40,9 @@ namespace MyApp } ``` -The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI -signature `(Landroid/os/Bundle;)V`. The app override need not repeat -`[Register]`: the scanner follows the registered base method through the -override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) -include both an explicitly registered `MainActivity` and an unannotated -`UserActivity` override. +The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI signature `(Landroid/os/Bundle;)V`. The app override need not repeat `[Register]`: the scanner follows the registered base method through the override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) include both an explicitly registered `MainActivity` and an unannotated `UserActivity` override. -The corresponding JCW is roughly the following Java. This excerpt omits -constructor and peer-reference code; its class name, registration call, -override, and native callback name match the -[JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): +The corresponding JCW is roughly the following Java. This excerpt omits constructor and peer-reference code; its class name, registration call, override, and native callback name match the [JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): ```java package my.app; @@ -83,26 +64,11 @@ public class MainActivity extends android.app.Activity } ``` -The Java name is `my/app/MainActivity`, not the assembly-qualified C# type -name. The method descriptor means one `android.os.Bundle` argument and a -`void` return; the `L...;` denotes an object reference. The generated -callback name is derived from binding metadata. `OnCreate` can call -`base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, -not a second JCW registration. +The Java name is `my/app/MainActivity`, not the assembly-qualified C# type name. The method descriptor means one `android.os.Bundle` argument and a `void` return; the `L...;` denotes an object reference. The generated callback name is derived from binding metadata. `OnCreate` can call `base.OnCreate` back into Java: that is a **new managed-to-Java invocation**, not a second JCW registration. ## Build-time representation -[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) -invokes the -[`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -on the application, references, and SDK/framework inputs. The scanner reads -managed assembly metadata, including Java type names, `[Register]` methods, -overrides of registered base/interface methods, constructors, `[Export]` -members, and manifest components. The -[`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) -validates the resulting model and feeds both the Java and managed emitters. -The app's generated JCWs do not come from the old -`JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. +[`GenerateTrimmableTypeMap`](../../../src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs) invokes the [`JavaPeerScanner`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) on the application, references, and SDK/framework inputs. The scanner reads managed assembly metadata, including Java type names, `[Register]` methods, overrides of registered base/interface methods, constructors, `[Export]` members, and manifest components. The [`TrimmableTypeMapGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs) validates the resulting model and feeds both the Java and managed emitters. The app's generated JCWs do not come from the old `JavaCallableWrapperGenerator` / `MarshalMethodsClassifier` pipeline. The main outputs are: @@ -114,15 +80,7 @@ The main outputs are: | `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | | `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | -Each per-assembly TypeMap contains assembly-level mapping attributes: a key -such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the -managed target type. The root references the per-assembly maps via -`TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, -not `.cs` files; the C# in the sections below is an explanatory translation -of emitted code, not a file that a developer should edit. See -[`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), -[`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), -and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). +Each per-assembly TypeMap contains assembly-level mapping attributes: a key such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the managed target type. The root references the per-assembly maps via `TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, not `.cs` files; the C# in the sections below is an explanatory translation of emitted code, not a file that a developer should edit. See [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). For the example Activity, think of a mapping like this **conceptual C#**: @@ -135,118 +93,40 @@ For the example Activity, think of a mapping like this **conceptual C#**: // IAndroidCallableWrapper. Proxy names are chosen by the emitter. ``` -There is no source-level `TypeMap` attribute for application code to write. -The real emitted `TypeMapAttribute` takes the Java key and proxy reference; -its trimmable three-argument form also takes the target reference. An -unconditional mapping has just the key and proxy reference, whereas the -target reference on a conditional mapping lets the trimming pipeline -discard entries for unused managed types. ACW proxies implement -`IAndroidCallableWrapper` to register callbacks; other proxies need not. -[`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -provides peer creation without `Activator.CreateInstance`. - -The -[shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) -run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, -and feed TypeMap DLLs to later steps. They track generated files and remove -stale JCWs when the managed model changes. The target's stamp and output lists -matter on incremental builds: a stale compiled Java class can otherwise -outlive its managed callback. - -CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) -give the TypeMap DLLs to ILLink as trimmable inputs, with -`_Microsoft.Android.TypeMaps` as the entry assembly. For -`PublishTrimmed=true`, a **second scan of linked assemblies** regenerates -JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the -pre-trim Java set is not necessarily the packaged set. NativeAOT's -[targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) -feed the generated DLLs to ILC and use its graph plus the ACW map when -producing Java shrinker configuration. Both modes need reachable managed -mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. +There is no source-level `TypeMap` attribute for application code to write. The real emitted `TypeMapAttribute` takes the Java key and proxy reference; its trimmable three-argument form also takes the target reference. An unconditional mapping has just the key and proxy reference, whereas the target reference on a conditional mapping lets the trimming pipeline discard entries for unused managed types. ACW proxies implement `IAndroidCallableWrapper` to register callbacks; other proxies need not. [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) provides peer creation without `Activator.CreateInstance`. + +The [shared TypeMap targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets) run `_GenerateTrimmableTypeMap` after `CoreCompile`, compile the generated Java, and feed TypeMap DLLs to later steps. They track generated files and remove stale JCWs when the managed model changes. The target's stamp and output lists matter on incremental builds: a stale compiled Java class can otherwise outlive its managed callback. + +CoreCLR's [runtime-specific targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets) give the TypeMap DLLs to ILLink as trimmable inputs, with `_Microsoft.Android.TypeMaps` as the entry assembly. For `PublishTrimmed=true`, a **second scan of linked assemblies** regenerates JCWs in `typemap/linked-java/` and records `linked-java-files.txt`; the pre-trim Java set is not necessarily the packaged set. NativeAOT's [targets](../../../src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets) feed the generated DLLs to ILC and use its graph plus the ACW map when producing Java shrinker configuration. Both modes need reachable managed mapping/proxy/entrypoint code; they do not resurrect the native LLVM TypeMap. ## Managed code calls Java -Android bindings are managed wrappers for *existing* Java APIs. For example, -[`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) -uses its bound member descriptor `"reportFullyDrawn.()V"` and calls -`_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The -essential C# shape (simplified) is: +Android bindings are managed wrappers for *existing* Java APIs. For example, [`Activity.ReportFullyDrawn()`](../../../src/Mono.Android/Android.App/Activity.cs) uses its bound member descriptor `"reportFullyDrawn.()V"` and calls `_members.InstanceMethods.InvokeVirtualVoidMethod (...)` on `this`. The essential C# shape (simplified) is: ```csharp const string id = "reportFullyDrawn.()V"; _members.InstanceMethods.InvokeVirtualVoidMethod (id, this, null); ``` -Java.Interop resolves the class and method, invokes JNI with the peer's -reference, and converts values/references according to the binding. Other -bound methods use `JniArgumentValue` arrays and the appropriate virtual, -nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a -per-call method dispatch table: it is involved when Java classes and managed -peers must be mapped or created, not in place of this invocation. - -When JNI returns an object or Java passes one into managed code, -[`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -uses the Java name and generated mappings to select a managed target and -proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -looks for an existing managed peer or constructs one from the JNI handle. -In the reverse direction, the type manager can obtain a JNI type signature -for a managed peer type. Do not confuse a bound `Activity` MCW with the -`MainActivity` JCW that implements Java-to-managed overrides. +Java.Interop resolves the class and method, invokes JNI with the peer's reference, and converts values/references according to the binding. Other bound methods use `JniArgumentValue` arrays and the appropriate virtual, nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a per-call method dispatch table: it is involved when Java classes and managed peers must be mapped or created, not in place of this invocation. + +When JNI returns an object or Java passes one into managed code, [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) uses the Java name and generated mappings to select a managed target and proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) looks for an existing managed peer or constructs one from the JNI handle. In the reverse direction, the type manager can obtain a JNI type signature for a managed peer type. Do not confuse a bound `Activity` MCW with the `MainActivity` JCW that implements Java-to-managed overrides. ## Java code calls managed code ### JCW declarations and per-class registration -The generated Java `onCreate` calls its native -`n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated -`nctor_*` native callbacks, and instantiate/activate the managed peer when -appropriate. The class's static block calls -`mono.android.Runtime.registerNatives (MainActivity.class)` when the class -is first initialized. This is **per Java class**. Application and -instrumentation JCWs instead use a guarded `__md_registerNatives()` helper -called from their generated wrappers: registration in their static -initializer can be too early in startup. The declaration of -`registerNatives(Class)` in -[`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) -is handwritten Java; individual JCWs and their registration calls are -generated. - -There is no concatenated `__md_methods` string and no -`Runtime.register("managed.Type, Assembly", ...)` registration in this path. -The old JNI native-symbol mangling rules (`Java_package_Class_method...`) -matter for symbol-lookup-based JNI code, but generated app JCW callbacks -here are associated with their Java declarations through -[`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): -the Java method **name** and **signature** must match; no `Java_...` export -is required for each method. +The generated Java `onCreate` calls its native `n_OnCreate_Landroid_os_Bundle_`; constructors similarly declare generated `nctor_*` native callbacks, and instantiate/activate the managed peer when appropriate. The class's static block calls `mono.android.Runtime.registerNatives (MainActivity.class)` when the class is first initialized. This is **per Java class**. Application and instrumentation JCWs instead use a guarded `__md_registerNatives()` helper called from their generated wrappers: registration in their static initializer can be too early in startup. The declaration of `registerNatives(Class)` in [`mono.android.Runtime`](../../../src/java-runtime/java/mono/android/Runtime.java) is handwritten Java; individual JCWs and their registration calls are generated. + +There is no concatenated `__md_methods` string and no `Runtime.register("managed.Type, Assembly", ...)` registration in this path. The old JNI native-symbol mangling rules (`Java_package_Class_method...`) matter for symbol-lookup-based JNI code, but generated app JCW callbacks here are associated with their Java declarations through [`RegisterNatives`](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html#registernatives): the Java method **name** and **signature** must match; no `Java_...` export is required for each method. ### Runtime startup and registration handshake -On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures -the VM at `JNI_OnLoad`, initializes the managed runtime from -`Runtime.initInternal`, and enters -[`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). -That installs `TrimmableTypeMapTypeManager` and -`TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls -`TrimmableTypeMap.RegisterNativeMethods()`. The old native -`Runtime.register` entrypoint remains a compatibility stub, **not** the -mechanism that registers every JCW. - -NativeAOT has its own entry: -[`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) -initialize the native host and a -[`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), -then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers -the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional -reflection-based peer construction path when a generated mapping is absent. -Its generated `ApplicationRegistration.registerApplications()` calls -`Runtime.registerNatives` for deferred startup types; the -[`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) -invokes it after `JavaInteropRuntime.init(...)`. - -The first native binding is for `mono.android.Runtime.registerNatives` -itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses actual C# UTF-8 string literals for its JNI name and signature: +On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures the VM at `JNI_OnLoad`, initializes the managed runtime from `Runtime.initInternal`, and enters [`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). That installs `TrimmableTypeMapTypeManager` and `TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls `TrimmableTypeMap.RegisterNativeMethods()`. The old native `Runtime.register` entrypoint remains a compatibility stub, **not** the mechanism that registers every JCW. + +NativeAOT has its own entry: [`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) initialize the native host and a [`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional reflection-based peer construction path when a generated mapping is absent. Its generated `ApplicationRegistration.registerApplications()` calls `Runtime.registerNatives` for deferred startup types; the [`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) invokes it after `JavaInteropRuntime.init(...)`. + +The first native binding is for `mono.android.Runtime.registerNatives` itself. [`TrimmableTypeMap.RegisterNativeMethods()`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) uses actual C# UTF-8 string literals for its JNI name and signature: ```csharp fixed (byte* name = "registerNatives"u8, @@ -256,26 +136,11 @@ fixed (byte* name = "registerNatives"u8, } ``` -When a JCW's static block (or deferred helper) calls this Java native -method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI -name, finds its mapping/proxy, and calls the proxy's -`IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the -class's `n_*` and `nctor_*` methods with ART. The handwritten -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -does the lookup and handoff; the *per-class* method table is in a -**generated managed TypeMap proxy**. The CoreCLR host does not supply a -table of exported native functions for these app methods. +When a JCW's static block (or deferred helper) calls this Java native method, `TrimmableTypeMap.OnRegisterNatives` obtains the class's JNI name, finds its mapping/proxy, and calls the proxy's `IAndroidCallableWrapper.RegisterNatives(JniType)`. This registers the class's `n_*` and `nctor_*` methods with ART. The handwritten [`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) does the lookup and handoff; the *per-class* method table is in a **generated managed TypeMap proxy**. The CoreCLR host does not supply a table of exported native functions for these app methods. ### Generated registration in C# terms, including UTF-8 -The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also -implements `IAndroidCallableWrapper`. For each native method it emits a -`JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI -signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` -method stack-allocates the entries and passes a `ReadOnlySpan` -to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and -callback identifiers are illustrative, and the emitter writes IL): +The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) emits sealed `JavaPeerProxy` types for peers that need proxies. A JCW proxy also implements `IAndroidCallableWrapper`. For each native method it emits a `JniNativeMethod` containing a **pointer to a name**, a **pointer to a JNI signature**, and a **callable entrypoint pointer**. Its `RegisterNatives` method stack-allocates the entries and passes a `ReadOnlySpan` to `JniEnvironment.Types.RegisterNatives`. In **conceptual C#** (field and callback identifiers are illustrative, and the emitter writes IL): ```csharp public unsafe void RegisterNatives (JniType jniType) @@ -290,65 +155,17 @@ public unsafe void RegisterNatives (JniType jniType) } ``` -The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) -emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in -RVA data**. It can reuse identical signature data. A representative -registration pair for the Activity example is -`n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these -identify the *native* Java declaration, not the C# method's -`GetOnCreate...Handler` connector. There is no runtime decoding of -`__md_methods` or per-callback `Java_...` export lookup. The handwritten -`registerNatives` binding above likewise supplies `byte*` name/signature -data, via C# `u8` literals. - -These name/signature bytes are not Java `String` argument marshalling. JNI's -`GetStringUTFChars`/`NewStringUTF` APIs use -[modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); -the emitter stores the registration identifiers/descriptors as UTF-8 -with explicit NUL terminators (the ASCII names/signatures above have -the same bytes in either encoding). For example, the CoreCLR -[class-name lookup](../../../src/native/clr/host/host-shared.cc) -obtains a Java class name via `GetStringUTFChars`, then converts its dots to -slashes for a TypeMap key. Separately, -[`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) -encodes method names/signatures as UTF-8 with an explicit trailing NUL -for native remapping lookup. Neither path is a substitute for marshalling -Java `String` values across JNI. +The [`PEAssemblyBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs) emits the actual name/signature fields as **NUL-terminated UTF-8 bytes in RVA data**. It can reuse identical signature data. A representative registration pair for the Activity example is `n_OnCreate_Landroid_os_Bundle_` and `(Landroid/os/Bundle;)V`; these identify the *native* Java declaration, not the C# method's `GetOnCreate...Handler` connector. There is no runtime decoding of `__md_methods` or per-callback `Java_...` export lookup. The handwritten `registerNatives` binding above likewise supplies `byte*` name/signature data, via C# `u8` literals. + +These name/signature bytes are not Java `String` argument marshalling. JNI's `GetStringUTFChars`/`NewStringUTF` APIs use [modified UTF-8](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/types.html#modified-utf-8-strings); the emitter stores the registration identifiers/descriptors as UTF-8 with explicit NUL terminators (the ASCII names/signatures above have the same bytes in either encoding). For example, the CoreCLR [class-name lookup](../../../src/native/clr/host/host-shared.cc) obtains a Java class name via `GetStringUTFChars`, then converts its dots to slashes for a TypeMap key. Separately, [`JniRemappingLookup`](../../../src/Mono.Android/Microsoft.Android.Runtime/JniRemappingLookup.cs) encodes method names/signatures as UTF-8 with an explicit trailing NUL for native remapping lookup. Neither path is a substitute for marshalling Java `String` values across JNI. ### From JNI ABI to a C# method -JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then -the declared arguments. Java object handles appear as pointer-sized -parameters; the generated managed entrypoints have JNI-ABI-compatible -signatures and are marked `UnmanagedCallersOnly`. For the example callback, -the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and -`IntPtr savedInstanceState`. This does **not** mean the app override accepts -`IntPtr`: its bound callback/proxy converts the `Bundle` reference and -dispatches the virtual C# `OnCreate(Bundle?)`. - -The scanner reads `[Register ("onCreate", -"(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` -on the bound method (or its base). The third string is **connector metadata**, -not the Java name of the native callback. For a normal registered binding, -the scanner uses it to find the callback's declaring type and name (such as -`n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR -signature. The -[`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -emits an `UnmanagedCallersOnly` forwarder to that existing callback, which -then calls the virtual managed method. The old path made a delegate using -`GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the -trimmable path derives callback metadata from the connector without -creating a delegate per registration. - -`[Export]` (and methods classified as directly callable) take a different -branch. The -[`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) -marks them for a generated direct dispatcher. The -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) -emits the JNI entrypoint and argument conversions, calls the managed target, -converts the return value, and copies back applicable array outputs; it -does not invoke a connector callback. For example, a Java-visible C# -method can be written as: +JNI passes a `JNIEnv*`, the instance (`jobject`) or class (`jclass`), then the declared arguments. Java object handles appear as pointer-sized parameters; the generated managed entrypoints have JNI-ABI-compatible signatures and are marked `UnmanagedCallersOnly`. For the example callback, the ABI parameters correspond to `IntPtr env`, `IntPtr self`, and `IntPtr savedInstanceState`. This does **not** mean the app override accepts `IntPtr`: its bound callback/proxy converts the `Bundle` reference and dispatches the virtual C# `OnCreate(Bundle?)`. + +The scanner reads `[Register ("onCreate", "(Landroid/os/Bundle;)V", "GetOnCreate_Landroid_os_Bundle_Handler")]` on the bound method (or its base). The third string is **connector metadata**, not the Java name of the native callback. For a normal registered binding, the scanner uses it to find the callback's declaring type and name (such as `n_OnCreate_Landroid_os_Bundle_`) and, when available, its *real* CLR signature. The [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) emits an `UnmanagedCallersOnly` forwarder to that existing callback, which then calls the virtual managed method. The old path made a delegate using `GetOnCreate...Handler` and `JNINativeWrapper.CreateDelegate`; the trimmable path derives callback metadata from the connector without creating a delegate per registration. + +`[Export]` (and methods classified as directly callable) take a different branch. The [`ModelBuilder`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs) marks them for a generated direct dispatcher. The [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) emits the JNI entrypoint and argument conversions, calls the managed target, converts the return value, and copies back applicable array outputs; it does not invoke a connector callback. For example, a Java-visible C# method can be written as: ```csharp [Java.Interop.Export ("handleClick")] @@ -358,125 +175,35 @@ public bool HandleClick (Android.Views.View view, int action) } ``` -This is the shape exercised by the -[export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). -The Java wrapper has a `native` declaration with its calculated JNI -signature; the generated managed dispatcher, not the developer's C# -method directly, is the unmanaged entrypoint. - -JNI signatures are **not** CLR method signatures. JNI `Z` is an -eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and -`Landroid/os/Bundle;` a Java object reference. The emitted entrypoint -uses JNI ABI types (a `byte` for a boolean); conversions such as -`jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# -`bool`. For normal registered callbacks the generator uses the -captured original callback signature where available: older bindings -may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. -It must not assume that the callback signature is identical to the -Java descriptor; see the scanner's -[callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) -and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). -No assembly rewriter is needed to insert separate `_mm_wrapper` methods. - -Both generated forwarders and direct dispatchers use a marshal-method -frame: `BeginMarshalMethod`, a `try` around the managed call, exception -handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in -`finally`. An arbitrary managed exception is not simply allowed to -escape across an unmanaged JNI boundary. Compare -[`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) -with the -[`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). +This is the shape exercised by the [export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). The Java wrapper has a `native` declaration with its calculated JNI signature; the generated managed dispatcher, not the developer's C# method directly, is the unmanaged entrypoint. + +JNI signatures are **not** CLR method signatures. JNI `Z` is an eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and `Landroid/os/Bundle;` a Java object reference. The emitted entrypoint uses JNI ABI types (a `byte` for a boolean); conversions such as `jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# `bool`. For normal registered callbacks the generator uses the captured original callback signature where available: older bindings may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. It must not assume that the callback signature is identical to the Java descriptor; see the scanner's [callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). No assembly rewriter is needed to insert separate `_mm_wrapper` methods. + +Both generated forwarders and direct dispatchers use a marshal-method frame: `BeginMarshalMethod`, a `try` around the managed call, exception handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in `finally`. An arbitrary managed exception is not simply allowed to escape across an unmanaged JNI boundary. Compare [`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) with the [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). The call sequence for a Java `onCreate(Bundle)` is therefore: 1. ART enters the generated `MainActivity.onCreate`. 2. That method calls its registered `n_OnCreate_Landroid_os_Bundle_`. -3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the - registered name/signature. For a bound override, it forwards to the - connector-derived bound callback; for a direct export it marshals and - invokes the target itself. -4. Peer lookup/activation and argument conversion supply the managed - `MainActivity` and `Bundle`; virtual dispatch runs the app override. - A call to `base.OnCreate` then goes **managed-to-Java** through - `JniPeerMembers`. +3. JNI invokes the generated `UnmanagedCallersOnly` entrypoint for the registered name/signature. For a bound override, it forwards to the connector-derived bound callback; for a direct export it marshals and invokes the target itself. +4. Peer lookup/activation and argument conversion supply the managed `MainActivity` and `Bundle`; virtual dispatch runs the app override. A call to `base.OnCreate` then goes **managed-to-Java** through `JniPeerMembers`. ## Peer activation and lifetime -A Java-side constructor and a managed-side constructor do not allocate -one shared object. Generated JCW constructors call `super(...)` and, -when the actual Java class matches the wrapper, a generated -`nctor_*` callback to activate the matching managed peer. This class -check matters when a JCW is further subclassed. For some startup -types registration is deferred as described above. The generator -produces the appropriate constructor shape from its scan; compare -[`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) -and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). - -On the managed side, -[`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) -uses generated mapping attributes to resolve a Java class and target -type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) -can create the managed object from a handle without late-bound -`Activator.CreateInstance`; activation also accounts for an existing -replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) -resolves names/types, while -[`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) -checks for registered peers and handles construction/ownership. - -Every generated JCW implements the Java -[`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) -hooks `monodroidAddReference` and `monodroidClearReferences`. Managed -[`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) -tracks add/peek/remove/finalize operations, and the GC bridge coordinates -the two collectors. Local and global JNI references have different -lifetimes: a managed wrapper may need a global reference to keep its -peer accessible after a JNI call returns. `JniHandleOwnership` records -whether an incoming reference is transferred. Calling `Dispose()` can -remove the association; a raw `IntPtr` retained afterward is not a -safe substitute for a live managed peer. For invalid reference -diagnostics, see -[Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). +A Java-side constructor and a managed-side constructor do not allocate one shared object. Generated JCW constructors call `super(...)` and, when the actual Java class matches the wrapper, a generated `nctor_*` callback to activate the matching managed peer. This class check matters when a JCW is further subclassed. For some startup types registration is deferred as described above. The generator produces the appropriate constructor shape from its scan; compare [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). + +On the managed side, [`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) uses generated mapping attributes to resolve a Java class and target type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) can create the managed object from a handle without late-bound `Activator.CreateInstance`; activation also accounts for an existing replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) resolves names/types, while [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) checks for registered peers and handles construction/ownership. + +Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtime/java/mono/android/IGCUserPeer.java) hooks `monodroidAddReference` and `monodroidClearReferences`. Managed [`JavaMarshalRegisteredPeers`](../../../src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs) tracks add/peek/remove/finalize operations, and the GC bridge coordinates the two collectors. Local and global JNI references have different lifetimes: a managed wrapper may need a global reference to keep its peer accessible after a JNI call returns. `JniHandleOwnership` records whether an incoming reference is transferred. Calling `Dispose()` can remove the association; a raw `IntPtr` retained afterward is not a safe substitute for a live managed peer. For invalid reference diagnostics, see [Debugging JNI Object Reference Crashes](debug-jni-objrefs.md). ## Diagnosing a broken interop path -Start at the boundary where the failure occurs; these files distinguish -build output, registration, and peer activation: - -1. **Missing or wrong Java class:** inspect the merged manifest, - `typemap/java/` (or CoreCLR's `typemap/linked-java/` after - trimming), `java-files.txt` / `linked-java-files.txt`, and - `acw-map.txt`. Confirm the expected Java name and base/override - signatures. A stale `.class` is not proof that a corresponding - TypeMap entry survived. -2. **Unbound native method / `UnsatisfiedLinkError`:** compare the - generated JCW's `native` method **name and JNI descriptor** with - its generated proxy registration. Check whether - `mono.android.Runtime.registerNatives` ran for the class (or its - deferred helper did), and inspect `adb logcat` for the registration - exception. Do not search for a missing `Java_...` export in this - pipeline. -3. **Wrong managed type or activation failure:** check - `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` - assembly, `TrimmableTypeMap` lookup, proxy construction, and - `JavaMarshalRegisteredPeers`. Verify whether a Java callback came - from a managed-created peer or needs activation from a Java-created - peer. On NativeAOT a missing generated mapping cannot be rescued - by CoreCLR-only reflection. -4. **Release/trim-only failure:** inspect the *linked* managed - assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and - shrinker rules. The pre-trim generated Java is not necessarily - packaged Java. The - [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) - explains target stamps and stale-file handling. -5. **JNI reference or marshal failure:** use `adb logcat` and - [the JNI reference guide](debug-jni-objrefs.md) for ownership and - collection problems. For unsupported TypeMap settings see - [the build property](../../docs-mobile/building-apps/build-properties.md); - duplicate Java-visible constructor signatures produce - [XA4259](../../docs-mobile/messages/xa4259.md). - -The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), -[JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), -and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) -exercise these paths. Compare their expected names/signatures against -the generated DLL and Java files before changing runtime registration. +Start at the boundary where the failure occurs; these files distinguish build output, registration, and peer activation: + +1. **Missing or wrong Java class:** inspect the merged manifest, `typemap/java/` (or CoreCLR's `typemap/linked-java/` after trimming), `java-files.txt` / `linked-java-files.txt`, and `acw-map.txt`. Confirm the expected Java name and base/override signatures. A stale `.class` is not proof that a corresponding TypeMap entry survived. +2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with its generated proxy registration. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. +3. **Wrong managed type or activation failure:** check `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` assembly, `TrimmableTypeMap` lookup, proxy construction, and `JavaMarshalRegisteredPeers`. Verify whether a Java callback came from a managed-created peer or needs activation from a Java-created peer. On NativeAOT a missing generated mapping cannot be rescued by CoreCLR-only reflection. +4. **Release/trim-only failure:** inspect the *linked* managed assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and shrinker rules. The pre-trim generated Java is not necessarily packaged Java. The [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) explains target stamps and stale-file handling. +5. **JNI reference or marshal failure:** use `adb logcat` and [the JNI reference guide](debug-jni-objrefs.md) for ownership and collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). + +The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), [JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) exercise these paths. Compare their expected names/signatures against the generated DLL and Java files before changing runtime registration. From 857ae8977ef77f3fecd24823ba4678e52c7c1e35 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 23 Sep 2026 09:49:39 +0200 Subject: [PATCH 48/58] Document unsupported explicit Java type registration Clarify the trimmable TypeMap behavior after the parent runtime review fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 10d9b25ef4d..0984361b13d 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -200,6 +200,8 @@ Every generated JCW implements the Java [`IGCUserPeer`](../../../src/java-runtim Start at the boundary where the failure occurs; these files distinguish build output, registration, and peer activation: +[`Java.Interop.TypeManager.RegisterType`](../../../src/Mono.Android/Java.Interop/TypeManager.cs) is not a fallback for a missing generated mapping: it throws `NotSupportedException` with the trimmable TypeMap. Give the peer a discoverable Java name (`[Register]` for a peer type, or the component `Name` attribute used above) so the scanner can generate the mapping instead. + 1. **Missing or wrong Java class:** inspect the merged manifest, `typemap/java/` (or CoreCLR's `typemap/linked-java/` after trimming), `java-files.txt` / `linked-java-files.txt`, and `acw-map.txt`. Confirm the expected Java name and base/override signatures. A stale `.class` is not proof that a corresponding TypeMap entry survived. 2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with its generated proxy registration. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. 3. **Wrong managed type or activation failure:** check `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` assembly, `TrimmableTypeMap` lookup, proxy construction, and `JavaMarshalRegisteredPeers`. Verify whether a Java callback came from a managed-created peer or needs activation from a Java-created peer. On NativeAOT a missing generated mapping cannot be rescued by CoreCLR-only reflection. From 751696dcef912262f8e6b13b808038165ba50594 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 24 Sep 2026 14:28:07 +0200 Subject: [PATCH 49/58] Document app-generated platform JCWs Clarify that trimmable app builds generate platform wrappers rather than using a prebuilt reference-pack jar, alongside the selected runtime support jar. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 0984361b13d..25601fccb2c 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -80,6 +80,8 @@ The main outputs are: | `typemap/typemap-assemblies.txt`, `java-files.txt`, `acw-map.txt` | Lists of actual generated assemblies/Java sources and Java-to-managed class mappings for the build. | | `android/src/net/dot/android/ApplicationRegistration.java`, merged `AndroidManifest.xml` | Generated application/instrumentation registration and manifest output. The former is built from scan results, **not** copied from a handwritten template. | +The app build also generates needed **platform JCWs** from framework inputs alongside user JCWs: for example, the [build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) check for `typemap/java/android/app/ActivityTracker.java` and its `Runtime.registerNatives` call. The [reference pack](../../../build-tools/create-packs/Microsoft.Android.Ref.proj) no longer ships prebuilt `mono.android.jar` or `mono.android.dex`. The handwritten Java runtime support instead comes from `java_runtime_trimmable.jar`, selected by [`_CollectRuntimeJarFilenames`](../../../src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets); this is not a jar of pre-generated platform JCWs. `ApplicationRegistration.java` is generated separately during the app build. + Each per-assembly TypeMap contains assembly-level mapping attributes: a key such as `my/app/MainActivity`, a proxy type, and, for a trimmable mapping, the managed target type. The root references the per-assembly maps via `TypeMapAssemblyTargetAttribute`. The emitter writes **IL and metadata**, not `.cs` files; the C# in the sections below is an explanatory translation of emitted code, not a file that a developer should edit. See [`TypeMapAssemblyGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs), [`TypeMapAssemblyEmitter.EmitTypeMapAttribute`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs), and the [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md). For the example Activity, think of a mapping like this **conceptual C#**: From 6d470c9c6bafc04f6d448c0a1235c0787a822f29 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 08:41:45 +0200 Subject: [PATCH 50/58] Point JNI interop guide at current generator sources Preserve the post-test-removal link corrections when replaying the guide on main. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index 25601fccb2c..cedc870673c 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -40,9 +40,9 @@ namespace MyApp } ``` -The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI signature `(Landroid/os/Bundle;)V`. The app override need not repeat `[Register]`: the scanner follows the registered base method through the override. The [scanner test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs) include both an explicitly registered `MainActivity` and an unannotated `UserActivity` override. +The binding for `Activity.OnCreate` carries the Java name `onCreate` and JNI signature `(Landroid/os/Bundle;)V`. The app override need not repeat `[Register]`: the [scanner](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) follows the registered base method through the override. -The corresponding JCW is roughly the following Java. This excerpt omits constructor and peer-reference code; its class name, registration call, override, and native callback name match the [JCW generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs): +The corresponding JCW is roughly the following Java. This excerpt omits constructor and peer-reference code; its class name, registration call, override, and native callback follow the [JCW generator](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs): ```java package my.app; @@ -177,9 +177,9 @@ public bool HandleClick (Android.Views.View view, int action) } ``` -This is the shape exercised by the [export fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). The Java wrapper has a `native` declaration with its calculated JNI signature; the generated managed dispatcher, not the developer's C# method directly, is the unmanaged entrypoint. +The [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) exercise generated `[Export]` methods. The Java wrapper has a `native` declaration with its calculated JNI signature; the generated managed dispatcher, not the developer's C# method directly, is the unmanaged entrypoint. -JNI signatures are **not** CLR method signatures. JNI `Z` is an eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and `Landroid/os/Bundle;` a Java object reference. The emitted entrypoint uses JNI ABI types (a `byte` for a boolean); conversions such as `jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# `bool`. For normal registered callbacks the generator uses the captured original callback signature where available: older bindings may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. It must not assume that the callback signature is identical to the Java descriptor; see the scanner's [callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs) and [test fixtures](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs). No assembly rewriter is needed to insert separate `_mm_wrapper` methods. +JNI signatures are **not** CLR method signatures. JNI `Z` is an eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and `Landroid/os/Bundle;` a Java object reference. The emitted entrypoint uses JNI ABI types (a `byte` for a boolean); conversions such as `jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# `bool`. For normal registered callbacks the generator uses the captured original callback signature where available: older bindings may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. It must not assume that the callback signature is identical to the Java descriptor; see the scanner's [callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs). No assembly rewriter is needed to insert separate `_mm_wrapper` methods. Both generated forwarders and direct dispatchers use a marshal-method frame: `BeginMarshalMethod`, a `try` around the managed call, exception handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in `finally`. An arbitrary managed exception is not simply allowed to escape across an unmanaged JNI boundary. Compare [`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) with the [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). @@ -192,7 +192,7 @@ The call sequence for a Java `onCreate(Bundle)` is therefore: ## Peer activation and lifetime -A Java-side constructor and a managed-side constructor do not allocate one shared object. Generated JCW constructors call `super(...)` and, when the actual Java class matches the wrapper, a generated `nctor_*` callback to activate the matching managed peer. This class check matters when a JCW is further subclassed. For some startup types registration is deferred as described above. The generator produces the appropriate constructor shape from its scan; compare [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) and its [constructor tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs). +A Java-side constructor and a managed-side constructor do not allocate one shared object. Generated JCW constructors call `super(...)` and, when the actual Java class matches the wrapper, a generated `nctor_*` callback to activate the matching managed peer. This class check matters when a JCW is further subclassed. For some startup types registration is deferred as described above. The [`JcwJavaSourceGenerator`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) emits the appropriate constructor shape from its scan. On the managed side, [`TrimmableTypeMap`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs) uses generated mapping attributes to resolve a Java class and target type. Its [`JavaPeerProxy`](../../../src/Mono.Android/Java.Interop/JavaPeerProxy.cs) can create the managed object from a handle without late-bound `Activator.CreateInstance`; activation also accounts for an existing replaceable peer. [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) resolves names/types, while [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) checks for registered peers and handles construction/ownership. @@ -210,4 +210,4 @@ Start at the boundary where the failure occurs; these files distinguish build ou 4. **Release/trim-only failure:** inspect the *linked* managed assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and shrinker rules. The pre-trim generated Java is not necessarily packaged Java. The [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) explains target stamps and stale-file handling. 5. **JNI reference or marshal failure:** use `adb logcat` and [the JNI reference guide](debug-jni-objrefs.md) for ownership and collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). -The [TypeMap generator tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs), [JCW tests](../../../tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs), and [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) exercise these paths. Compare their expected names/signatures against the generated DLL and Java files before changing runtime registration. +The [TypeMap generator](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs) and [JCW generator](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs) implement these paths; the [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs) cover generated files and packaging. Compare the emitted names/signatures against the generated DLL and Java files before changing runtime registration. From 222dccfac3ff46c91105d7cc478f88256b49bd38 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 09:08:25 +0200 Subject: [PATCH 51/58] Clarify JNI virtual dispatch and managed registration Explain that virtual Java dispatch may round-trip into a generated JCW callback and point JNI troubleshooting at managed IAndroidCallableWrapper registrations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index cedc870673c..b7aca6ef5e4 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -110,7 +110,7 @@ const string id = "reportFullyDrawn.()V"; _members.InstanceMethods.InvokeVirtualVoidMethod (id, this, null); ``` -Java.Interop resolves the class and method, invokes JNI with the peer's reference, and converts values/references according to the binding. Other bound methods use `JniArgumentValue` arrays and the appropriate virtual, nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a per-call method dispatch table: it is involved when Java classes and managed peers must be mapped or created, not in place of this invocation. +Java.Interop resolves the class and method, invokes JNI with the peer's reference, and converts values/references according to the binding. This call enters Java, not a JCW callback directly; Java virtual dispatch can subsequently reach a managed subclass's generated JCW override and call its registered `n_*` method back into managed code. The two directions can therefore form a managed-to-Java-to-managed round trip. Other bound methods use `JniArgumentValue` arrays and the appropriate virtual, nonvirtual, or static `JniPeerMembers` invocation. The TypeMap is not a per-call method dispatch table: it is involved when Java classes and managed peers must be mapped or created, not in place of this invocation. When JNI returns an object or Java passes one into managed code, [`TrimmableTypeMapTypeManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs) uses the Java name and generated mappings to select a managed target and proxy. [`TrimmableTypeMapValueManager`](../../../src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs) looks for an existing managed peer or constructs one from the JNI handle. In the reverse direction, the type manager can obtain a JNI type signature for a managed peer type. Do not confuse a bound `Activity` MCW with the `MainActivity` JCW that implements Java-to-managed overrides. @@ -205,7 +205,7 @@ Start at the boundary where the failure occurs; these files distinguish build ou [`Java.Interop.TypeManager.RegisterType`](../../../src/Mono.Android/Java.Interop/TypeManager.cs) is not a fallback for a missing generated mapping: it throws `NotSupportedException` with the trimmable TypeMap. Give the peer a discoverable Java name (`[Register]` for a peer type, or the component `Name` attribute used above) so the scanner can generate the mapping instead. 1. **Missing or wrong Java class:** inspect the merged manifest, `typemap/java/` (or CoreCLR's `typemap/linked-java/` after trimming), `java-files.txt` / `linked-java-files.txt`, and `acw-map.txt`. Confirm the expected Java name and base/override signatures. A stale `.class` is not proof that a corresponding TypeMap entry survived. -2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with its generated proxy registration. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. +2. **Unbound native method / `UnsatisfiedLinkError`:** compare the generated JCW's `native` method **name and JNI descriptor** with the generated managed `IAndroidCallableWrapper.RegisterNatives` entry for that class. Check whether `mono.android.Runtime.registerNatives` ran for the class (or its deferred helper did), and inspect `adb logcat` for the registration exception. Do not search for a missing `Java_...` export in this pipeline. 3. **Wrong managed type or activation failure:** check `typemap-assemblies.txt`, the root `_Microsoft.Android.TypeMaps` assembly, `TrimmableTypeMap` lookup, proxy construction, and `JavaMarshalRegisteredPeers`. Verify whether a Java callback came from a managed-created peer or needs activation from a Java-created peer. On NativeAOT a missing generated mapping cannot be rescued by CoreCLR-only reflection. 4. **Release/trim-only failure:** inspect the *linked* managed assemblies and CoreCLR post-trim JCWs, or NativeAOT ILC inputs and shrinker rules. The pre-trim generated Java is not necessarily packaged Java. The [build-pipeline guide](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/README.md) explains target stamps and stale-file handling. 5. **JNI reference or marshal failure:** use `adb logcat` and [the JNI reference guide](debug-jni-objrefs.md) for ownership and collection problems. For unsupported TypeMap settings see [the build property](../../docs-mobile/building-apps/build-properties.md); duplicate Java-visible constructor signatures produce [XA4259](../../docs-mobile/messages/xa4259.md). From c39061b34c95d55fadd04f321542305678b0ccc3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 09:12:10 +0200 Subject: [PATCH 52/58] Remove orphaned native linking generator test The native runtime linking generator was removed by the LLVM IR rewrite; its standalone test cannot compile after the parent gained it. Keep the remaining JNI and application config coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...enerateNativeRuntimeLinkingSourcesTests.cs | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeRuntimeLinkingSourcesTests.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeRuntimeLinkingSourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeRuntimeLinkingSourcesTests.cs deleted file mode 100644 index 4400508e8c7..00000000000 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateNativeRuntimeLinkingSourcesTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using Mono.Cecil; -using NUnit.Framework; -using Xamarin.Android.Tasks; - -namespace Xamarin.Android.Build.Tests; - -[TestFixture] -public class GenerateNativeRuntimeLinkingSourcesTests : BaseTest -{ - [TestCase ("linked")] - [TestCase ("R2R")] - public void NativeLinkingIgnoresDuplicateNonFrameworkAssemblies (string finalDirectory) - { - var path = Path.Combine (Root, "temp", TestName); - Directory.CreateDirectory (path); - try { - var arm64Framework = CreateAssembly (Path.Combine (path, "android-arm64", finalDirectory), - "Framework", "arm64-v8a", "SystemNative_Arm64", framework: true); - var x64Framework = CreateAssembly (Path.Combine (path, "android-x64", finalDirectory), - "Framework", "x86_64", "SystemNative_X64", framework: true); - var resolvedAssemblies = new List { arm64Framework, x64Framework }; - foreach (var name in new [] { "_Microsoft.Android.TypeMaps", "_Java.Interop.TypeMap" }) { - resolvedAssemblies.Add (CreateAssembly (Path.Combine (path, "android-arm64", finalDirectory), - name, "arm64-v8a", "SystemNative_FinalTypeMap", framework: false)); - resolvedAssemblies.Add (CreateAssembly (Path.Combine (path, "android-x64", finalDirectory), - name, "x86_64", "SystemNative_FinalTypeMap", framework: false)); - var preTrim = CreateAssembly (Path.Combine (path, "typemap"), name, "arm64-v8a", - "SystemNative_PreTrimTypeMap", framework: false); - preTrim.SetMetadata ("_AndroidPreTrimTypeMapCandidate", "true"); - resolvedAssemblies.Add (preTrim); - } - var userAssembly = CreateAssembly (Path.Combine (path, "user"), "User", "arm64-v8a", - "SystemNative_User", framework: false); - userAssembly.RemoveMetadata ("FrameworkAssembly"); - resolvedAssemblies.Add (userAssembly); - resolvedAssemblies.Add (userAssembly); - var errors = new List (); - var task = new GenerateNativeRuntimeLinkingSources { - BuildEngine = new MockBuildEngine (TestContext.Out, errors), - EnvironmentOutputDirectory = path, - ResolvedAssemblies = resolvedAssemblies.ToArray (), - SupportedAbis = ["arm64-v8a", "x86_64"], - }; - - Assert.IsTrue (task.Execute (), "Native linking must ignore pre-trim and final typemap duplicates before ABI indexing."); - Assert.IsEmpty (errors); - var arm64 = File.ReadAllText (Path.Combine (path, "pinvoke_preserve.arm64-v8a.ll")); - var x64 = File.ReadAllText (Path.Combine (path, "pinvoke_preserve.x86_64.ll")); - StringAssert.Contains ("@SystemNative_Arm64", arm64); - StringAssert.DoesNotContain ("@SystemNative_X64", arm64); - StringAssert.Contains ("@SystemNative_X64", x64); - StringAssert.DoesNotContain ("@SystemNative_Arm64", x64); - foreach (var output in new [] { arm64, x64 }) { - StringAssert.DoesNotContain ("@SystemNative_FinalTypeMap", output); - StringAssert.DoesNotContain ("@SystemNative_PreTrimTypeMap", output); - StringAssert.DoesNotContain ("@SystemNative_User", output); - } - foreach (var abi in task.SupportedAbis) - Assert.IsFalse (File.Exists (Path.Combine (path, $"marshal_methods.{abi}.ll")), "Legacy marshal-method stubs must not be generated."); - } finally { - Directory.Delete (path, recursive: true); - } - } - - static TaskItem CreateAssembly (string directory, string name, string abi, string entryPoint, bool framework) - { - Directory.CreateDirectory (directory); - var path = Path.Combine (directory, $"{name}.dll"); - using var assembly = AssemblyDefinition.CreateAssembly ( - new AssemblyNameDefinition (name, new Version (1, 0)), name, ModuleKind.Dll); - var module = assembly.MainModule; - var nativeModule = new ModuleReference ("System.Native"); - module.ModuleReferences.Add (nativeModule); - var type = new TypeDefinition ("Example", "NativeMethods", TypeAttributes.Public, module.TypeSystem.Object); - type.Methods.Add (new MethodDefinition ("Invoke", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.PInvokeImpl, - module.TypeSystem.Void) { - PInvokeInfo = new PInvokeInfo (PInvokeAttributes.CallConvCdecl, entryPoint, nativeModule), - }); - module.Types.Add (type); - assembly.Write (path); - var item = new TaskItem (path); - item.SetMetadata ("Abi", abi); - item.SetMetadata ("FrameworkAssembly", framework.ToString ()); - return item; - } -} From 0f50ebf53fe1c18d0a0d29eaf93d948ce14c10f4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 11:51:08 +0200 Subject: [PATCH 53/58] Guard nullable JNI remapping method signatures Handle missing and empty signatures explicitly before accessing their final character, so the build tasks compile with nullable warnings as errors while retaining exact, parameter-only, and wildcard ordering. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Utilities/JniRemappingAssemblyGenerator.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs index da311461c7a..30e96a8a531 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JniRemappingAssemblyGenerator.cs @@ -199,8 +199,13 @@ static List SortMethods (List input) return result; } - static int SignatureSpecificity (string? signature) => - string.IsNullOrEmpty (signature) ? 2 : signature [signature.Length - 1] == ')' ? 1 : 0; + static int SignatureSpecificity (string? signature) + { + if (signature == null || signature.Length == 0) { + return 2; + } + return signature [signature.Length - 1] == ')' ? 1 : 0; + } static List SortFields (List input) { From 9a1e0400545b6adbb63a35ad9481143c56883fec Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 12:02:35 +0200 Subject: [PATCH 54/58] Root Android runtime tests before NativeAOT ILC The NativeAOT build skips ILLink, so adding the test assemblies only before ILLink leaves ILC without their NUnit fixtures. Root them before WriteIlcRspFileForCompilation as well. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Mono.Android-Tests/Mono.Android.NET-Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 722deb4563d..9859174d2c4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -95,7 +95,7 @@ + BeforeTargets="PrepareForILLink;_RunILLink;WriteIlcRspFileForCompilation"> From 9893d8dd5a0c70d11d09361659cc6df501283add Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 13:23:31 +0200 Subject: [PATCH 55/58] Correct JNI registration and callback-frame documentation Clarify that Runtime.register was removed and distinguish normal callback forwarders from direct export and constructor transition frames. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- Documentation/guides/internals/JavaJNI_Interop.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/guides/internals/JavaJNI_Interop.md b/Documentation/guides/internals/JavaJNI_Interop.md index b7aca6ef5e4..7df5f171209 100644 --- a/Documentation/guides/internals/JavaJNI_Interop.md +++ b/Documentation/guides/internals/JavaJNI_Interop.md @@ -124,7 +124,7 @@ There is no concatenated `__md_methods` string and no `Runtime.register("managed ### Runtime startup and registration handshake -On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures the VM at `JNI_OnLoad`, initializes the managed runtime from `Runtime.initInternal`, and enters [`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). That installs `TrimmableTypeMapTypeManager` and `TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls `TrimmableTypeMap.RegisterNativeMethods()`. The old native `Runtime.register` entrypoint remains a compatibility stub, **not** the mechanism that registers every JCW. +On CoreCLR, the [native host](../../../src/native/clr/host/host.cc) captures the VM at `JNI_OnLoad`, initializes the managed runtime from `Runtime.initInternal`, and enters [`JNIEnvInit.Initialize`](../../../src/Mono.Android/Android.Runtime/JNIEnvInit.cs). That installs `TrimmableTypeMapTypeManager` and `TrimmableTypeMapValueManager` in `AndroidRuntime`, then calls `TrimmableTypeMap.RegisterNativeMethods()`. The old Java `Runtime.register` declaration and its native entrypoint have been removed; each JCW now uses its generated managed `IAndroidCallableWrapper.RegisterNatives` implementation. NativeAOT has its own entry: [`JavaInteropRuntime.JNI_OnLoad` and `init`](../../../src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs) initialize the native host and a [`JreRuntime`](../../../src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs), then call `JNIEnvInit.InitializeNativeAotRuntime()`. That path also registers the `TrimmableTypeMap` callback. NativeAOT cannot rely on CoreCLR's optional reflection-based peer construction path when a generated mapping is absent. Its generated `ApplicationRegistration.registerApplications()` calls `Runtime.registerNatives` for deferred startup types; the [`NativeAotRuntimeProvider`](../../../src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java) invokes it after `JavaInteropRuntime.init(...)`. @@ -181,7 +181,7 @@ The [application build tests](../../../src/Xamarin.Android.Build.Tasks/Tests/Xam JNI signatures are **not** CLR method signatures. JNI `Z` is an eight-bit `jboolean`, `C` a 16-bit `jchar`, `I` a 32-bit `jint`, and `Landroid/os/Bundle;` a Java object reference. The emitted entrypoint uses JNI ABI types (a `byte` for a boolean); conversions such as `jniValue != 0` and `managedResult ? (byte) 1 : (byte) 0` bridge C# `bool`. For normal registered callbacks the generator uses the captured original callback signature where available: older bindings may declare `bool`/`char` while newer ones can use `sbyte`/`ushort`. It must not assume that the callback signature is identical to the Java descriptor; see the scanner's [callback signature handling](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs). No assembly rewriter is needed to insert separate `_mm_wrapper` methods. -Both generated forwarders and direct dispatchers use a marshal-method frame: `BeginMarshalMethod`, a `try` around the managed call, exception handoff to `OnUserUnhandledException`, and `EndMarshalMethod` in `finally`. An arbitrary managed exception is not simply allowed to escape across an unmanaged JNI boundary. Compare [`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) with the [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs). +The generated entrypoint handles exceptions differently depending on the callback. A normal `[Register]` forwarder waits for GC bridge processing, calls the bound `n_*` callback in a `try`, and sends caught exceptions to `AndroidEnvironmentInternal.UnhandledException`; [`EmitUcoForwarderBody`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs) does **not** create a `BeginMarshalMethod`/`EndMarshalMethod` frame. Modern bound callbacks can establish that JNI transition inside [`JniMarshal.SafeInvoke`](../../../external/Java.Interop/src/Java.Interop/Java.Interop/JniMarshal.SafeInvoke.cs) or [`JniMarshalTyped`](../../../src/Mono.Android/Java.Interop/JniMarshalTyped.cs). Generated direct `[Export]` dispatchers and constructor entrypoints instead call `BeginMarshalMethod`, pass caught exceptions to `JniRuntime.OnUserUnhandledException`, and call `EndMarshalMethod` in `finally`; see [`ExportMethodDispatchEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs) and the generated constructor body in [`TypeMapAssemblyEmitter`](../../../src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs). Inspect the callback kind to find the owner of JNI transition and exception handling. The call sequence for a Java `onCreate(Bundle)` is therefore: From 24ea425b965b86d95a321839d953e6ea96d43c2d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 5 Oct 2026 21:15:13 +0200 Subject: [PATCH 56/58] Drop JVM-only Java.Interop test runs Retain the main branch test-library trimmer root while rooting NUnit assemblies at ILLink and NativeAOT input collection time. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../skills/tests/references/test-catalog.md | 3 +- .../automation/azure-pipelines-internal.yaml | 12 - .../automation/azure-pipelines-public.yaml | 14 - build-tools/automation/azure-pipelines.yaml | 11 - .../stage-java-interop-tests.yaml | 131 --- .../Java.Interop-Tests.csproj | 7 - .../CallVirtualFromConstructorBase.cs | 55 -- .../CallVirtualFromConstructorDerived.cs | 101 --- .../InvokeVirtualFromConstructorTests.cs | 156 ---- .../Java.Interop/JavaManagedGCBridgeTests.cs | 69 -- .../Java.Interop/JavaObjectArrayTest.cs | 36 - .../Java.Interop/JavaObjectTest.cs | 13 +- .../JavaPeerableExtensionsTests.cs | 4 - .../Java.Interop/JavaVMFixture.cs | 32 - .../Java.Interop/JniInstanceMethodIDTest.cs | 41 - .../Java.Interop/JniPeerMembersTests.cs | 170 ---- .../JniRuntime.JniTypeManagerTests.cs | 116 --- .../JniRuntime.JniValueManagerTests.cs | 149 ---- .../JniRuntimeJniValueManagerContract.cs | 535 ------------ .../Java.Interop/JniRuntimeTest.cs | 53 -- .../Java.Interop/JniTypeManagerTests.cs | 12 - .../JniValueMarshalerAttributeTests.cs | 53 +- .../JniValueMarshalerContractTests.cs | 770 ------------------ .../Java.Interop/MethodBindingTests.cs | 79 -- .../Java.Interop/TestTypeTests.cs | 1 - .../jni/test/AnotherJavaInterfaceImpl.java | 13 - .../test/CallVirtualFromConstructorBase.java | 38 - .../CallVirtualFromConstructorDerived.java | 47 -- .../dot/jni/test/CrossReferenceBridge.java | 32 - .../net/dot/jni/test/RenameClassBase1.java | 39 - .../net/dot/jni/test/RenameClassBase2.java | 45 - .../net/dot/jni/test/RenameClassDerived.java | 45 - .../Xamarin.Android.JcwGen-Tests.csproj | 11 +- .../Java.Interop-Tests.NET.csproj | 18 - .../Mono.Android.NET-Tests.csproj | 16 +- .../Mono.Android-Tests/Remaps.xml | 9 - .../TestInstrumentation.cs | 3 - 37 files changed, 74 insertions(+), 2865 deletions(-) delete mode 100644 build-tools/automation/yaml-templates/stage-java-interop-tests.yaml delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java delete mode 100644 external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java diff --git a/.github/skills/tests/references/test-catalog.md b/.github/skills/tests/references/test-catalog.md index b06734e5171..d285a8a6dbc 100644 --- a/.github/skills/tests/references/test-catalog.md +++ b/.github/skills/tests/references/test-catalog.md @@ -145,8 +145,7 @@ Device: **Yes** The `Mono.Android.NET-Tests.csproj` dynamically excludes categories based on runtime: - **CoreCLR runtime**: Excludes `CoreCLRIgnore`, `NTLM` -- **NativeAOT runtime**: Excludes `NativeAOTIgnore`, `SSL`, `NTLM`; excludes `Export` only when the trimmable typemap is off -- **Trimmable typemap**: Excludes `NativeTypeMap`, `TrimmableTypeMapUnsupported` +- **NativeAOT runtime**: Excludes `NativeAOTIgnore`, `SSL`, `NTLM` Other categories: `SSL`, `InetAccess`, `JavaList`, `RuntimeConfig`, `Intune`, `NTLM` diff --git a/build-tools/automation/azure-pipelines-internal.yaml b/build-tools/automation/azure-pipelines-internal.yaml index 6f82b742a19..ecb7330fa23 100644 --- a/build-tools/automation/azure-pipelines-internal.yaml +++ b/build-tools/automation/azure-pipelines-internal.yaml @@ -820,18 +820,6 @@ extends: parameters: condition: true - # Java.Interop Tests Stage - - template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - windowsPool: - name: $(NetCoreInternalPoolName) - image: $(WindowsPoolImageNetCoreInternal) - os: windows - macPool: - name: Azure Pipelines - vmImage: $(HostedMacImage) - os: macOS - # MAUI Tests Stage - stage: maui_tests displayName: MAUI Tests diff --git a/build-tools/automation/azure-pipelines-public.yaml b/build-tools/automation/azure-pipelines-public.yaml index 472a887718f..551e4c27fb1 100644 --- a/build-tools/automation/azure-pipelines-public.yaml +++ b/build-tools/automation/azure-pipelines-public.yaml @@ -432,20 +432,6 @@ stages: parameters: condition: true -# Java.Interop Tests Stage -- template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - windowsPool: - name: $(NetCorePublicPoolName) - demands: - - ImageOverride -equals $(WindowsPoolImageNetCorePublic) - os: windows - macPool: - name: AcesShared - demands: - - ImageOverride -equals ACES_VM_SharedPool_Tahoe - os: macOS - # Android Tools Tests Stage - template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self parameters: diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml index 14ff7856d3b..14bec27e024 100644 --- a/build-tools/automation/azure-pipelines.yaml +++ b/build-tools/automation/azure-pipelines.yaml @@ -106,17 +106,6 @@ extends: parameters: usesCleanImages: ${{ parameters.macTestAgentsUseCleanImages }} - - template: /build-tools/automation/yaml-templates/stage-java-interop-tests.yaml@self - parameters: - # The internal Xamarin.Android pipeline uses 1ES Pipeline Templates, which - # require Windows jobs to run on a 1ES-hosted pool. macOS is fine on the - # hosted Azure Pipelines pool as long as `os: macOS` is set (which is now - # the template default). - windowsPool: - name: MAUI-1ESPT - image: $(WindowsPoolImage1ESPT) - os: windows - - template: /build-tools/automation/yaml-templates/stage-xamarin-android-tools-tests.yaml@self parameters: use1ESTemplate: true diff --git a/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml b/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml deleted file mode 100644 index 92aff51cf19..00000000000 --- a/build-tools/automation/yaml-templates/stage-java-interop-tests.yaml +++ /dev/null @@ -1,131 +0,0 @@ -# Java.Interop tests stage -# -# Mirrors the two jobs from -# external/Java.Interop/build-tools/automation/azure-pipelines.yaml -# (windows_dotnet_build, mac_dotnet_build) so that the Java.Interop -# build + tests continue to run as part of dotnet/android CI now that -# Java.Interop has been merged in-tree. -# -# Referenced from both: -# - build-tools/automation/azure-pipelines.yaml (official / 1ES) -# - build-tools/automation/azure-pipelines-public.yaml (public PR validation) - -parameters: -- name: stageName - type: string - default: java_interop_tests -- name: dependsOn - type: object - default: [] -- name: condition - type: string - default: succeeded() -- name: windowsPool - type: object - # NOTE: This default is a Microsoft-hosted Azure Pipelines pool, which is NOT - # allowed by 1ES Pipeline Templates in Official mode. Callers that extend the - # 1ES Official template (e.g. build-tools/automation/azure-pipelines.yaml) must - # override this with a 1ES-hosted pool like `MAUI-1ESPT`. - default: - name: Azure Pipelines - vmImage: $(HostedWinImage) - os: windows -- name: macPool - type: object - default: - name: Azure Pipelines - vmImage: $(HostedMacImage) - os: macOS - -stages: -- stage: ${{ parameters.stageName }} - displayName: Java.Interop Tests - dependsOn: ${{ parameters.dependsOn }} - condition: ${{ parameters.condition }} - variables: - # Variables consumed by external/Java.Interop/build-tools/automation/templates/*.yaml. - # Defined at stage scope so they don't conflict with dotnet/android-wide - # variables (e.g. DotNetTargetFramework=net11.0). - DotNetCoreVersion: $(DotNetSdkVersion).x - DotNetTargetFramework: $(DotNetStableTargetFramework) - NetCoreTargetFrameworkPathSuffix: -$(DotNetStableTargetFramework) - Build.Configuration: Release - RunningOnCI: true - # Cap the JDK that external/Java.Interop's Prepare.targets picks when it - # calls the JdkInfo bootstrap task. Recent hosted agent images ship Java 25 - # as the default JDK, and Gradle 8.12 (used by java-source-utils and the - # Bytecode-Tests kotlin-gradle project) crashes with "Unsupported class - # file major version 69" on it. JdkInfo reads JI_MAX_JDK from the env. - JI_MAX_JDK: 21 - jobs: - - # Check - "Xamarin.Android (Java.Interop Tests Windows - .NET)" - - job: java_interop_windows_dotnet_build - displayName: Windows - .NET - pool: ${{ parameters.windowsPool }} - timeoutInMinutes: 60 - workspace: - clean: all - steps: - - checkout: self - submodules: recursive - clean: true - - - template: /external/Java.Interop/build-tools/automation/templates/install-dependencies.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-build.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-tests.yaml@self - parameters: - runNativeDotnetTests: true - nativeAotRid: win-x64 - platformName: .NET - Windows - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-dirty-tree.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-issue.yaml@self - - # Check - "Xamarin.Android (Java.Interop Tests Mac - .NET)" - - job: java_interop_mac_dotnet_build - displayName: Mac - .NET - pool: ${{ parameters.macPool }} - timeoutInMinutes: 20 - workspace: - clean: all - steps: - - checkout: self - submodules: recursive - clean: true - - - bash: | - set -e - if ! command -v cmake >/dev/null 2>&1 ; then - brew install cmake - fi - displayName: Ensure cmake is installed - - # The hosted macOS pool architecture can change between Intel (osx-x64) and - # Apple Silicon (osx-arm64), so detect it at runtime and publish/run the - # NativeAOT sample for the matching RID. Otherwise the produced .dylib fails - # to dlopen with an "incompatible architecture" error. - - bash: | - if [ "$(Agent.OSArchitecture)" = "ARM64" ]; then - echo "##vso[task.setvariable variable=NativeAOTRid]osx-arm64" - else - echo "##vso[task.setvariable variable=NativeAOTRid]osx-x64" - fi - displayName: Determine NativeAOT RID - - - template: /external/Java.Interop/build-tools/automation/templates/install-dependencies.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-build.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/core-tests.yaml@self - parameters: - runNativeTests: true - nativeAotRid: $(NativeAOTRid) - platformName: .NET - MacOS - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-dirty-tree.yaml@self - - - template: /external/Java.Interop/build-tools/automation/templates/fail-on-issue.yaml@self diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 7df4b7ff57c..5743ebeedf9 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -31,23 +31,16 @@ - - - - - - - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs deleted file mode 100644 index ce298734b93..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; -namespace Java.InteropTests { - - [JniTypeSignature (CallVirtualFromConstructorBase.JniTypeName, GenerateJavaPeer=false)] - public class CallVirtualFromConstructorBase : JavaObject { - - internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorBase"; - readonly static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorBase)); - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - public unsafe CallVirtualFromConstructorBase (int value) - : this (value, useNewObject: false) - { - } - - public unsafe CallVirtualFromConstructorBase (int value, bool useNewObject) - : this (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - if (PeerReference.IsValid) - return; - - const string __id = "(I)V"; - - if (useNewObject) { - var ctors = JniPeerMembers.InstanceMethods.GetConstructorsForType (GetType ()); - var init = ctors.GetConstructor (__id); - - JniArgumentValue* __args = stackalloc JniArgumentValue [1]; - __args [0] = new JniArgumentValue (value); - var lref = JniEnvironment.Object.NewObject (ctors.JniPeerType.PeerReference, init, __args); - Construct (ref lref, JniObjectReferenceOptions.CopyAndDispose); - return; - } - var peer = JniPeerMembers.InstanceMethods.StartGenericCreateInstance (__id, GetType (), value); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - JniPeerMembers.InstanceMethods.FinishGenericCreateInstance (__id, this, value); - } - - public CallVirtualFromConstructorBase (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref reference, options) - { - } - - public virtual void CalledFromConstructor (int value) - { - _members.InstanceMethods.InvokeGenericVirtualVoidMethod ("calledFromConstructor.(I)V", this, value); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs deleted file mode 100644 index 739b85e08c9..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs +++ /dev/null @@ -1,101 +0,0 @@ -#nullable enable - -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; - -namespace Java.InteropTests -{ - [JniTypeSignature (CallVirtualFromConstructorDerived.JniTypeName, GenerateJavaPeer=false)] - public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase { - new internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorDerived"; - static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorDerived)); - - [JniAddNativeMethodRegistrationAttribute] - static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) - { - args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (CalledFromConstructorMarshalMethod)CalledFromConstructorHandler)); - } - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - int calledValue; - - public bool InvokedConstructor; - - public CallVirtualFromConstructorDerived (int value) - : this (value, useNewObject: false) - { - } - - public CallVirtualFromConstructorDerived (int value, bool useNewObject) - : base (value, useNewObject) - { - InvokedConstructor = true; - - if (useNewObject && calledValue != 0) { - // calledValue was set on a *different* instance! So it's 0 here. - throw new ArgumentException ( - string.Format ("value '{0}' doesn't match expected value '{1}'.", value, 0), - "value"); - } - if (!useNewObject && value != calledValue) - throw new ArgumentException ( - string.Format ("value '{0}' doesn't match expected value '{1}'.", value, calledValue), - "value"); - } - - public bool InvokedActivationConstructor; - - public static CallVirtualFromConstructorDerived? Intermediate_FromCalledFromConstructor; - public static CallVirtualFromConstructorDerived? Intermediate_FromActivationConstructor; - - public CallVirtualFromConstructorDerived (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref reference, options) - { - InvokedActivationConstructor = true; - - Intermediate_FromActivationConstructor = this; - } - - public bool Called; - - public override void CalledFromConstructor (int value) - { - Called = true; - calledValue = value; - - Intermediate_FromCalledFromConstructor = this; - } - - public static unsafe CallVirtualFromConstructorDerived NewInstance (int value) - { - JniArgumentValue* args = stackalloc JniArgumentValue [1]; - args [0] = new JniArgumentValue (value); - var o = _members.StaticMethods.InvokeObjectMethod ("newInstance.(I)Lnet/dot/jni/test/CallVirtualFromConstructorDerived;", args); - return JniEnvironment.Runtime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)!; - } - - delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value); - static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int value) - { - var envp = new JniTransition (jnienv); - try { - var r_self = new JniObjectReference (n_self); - var self = JniEnvironment.Runtime.ValueManager.GetValue(ref r_self, JniObjectReferenceOptions.Copy)!; - self.CalledFromConstructor (value); - self.DisposeUnlessReferenced (); - } - catch (Exception e) when (JniEnvironment.Runtime.ExceptionShouldTransitionToJni (e)) { - envp.SetPendingException (e); - } - finally { - envp.Dispose (); - } - } - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs deleted file mode 100644 index 949f1902997..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] -#if !__ANDROID__ - // We want stability around the CallVirtualFromConstructorDerived static fields - [NonParallelizable] -#endif // !__ANDROID__ - public class InvokeVirtualFromConstructorTests : JavaVMFixture - { - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateManagedInstanceFirst_WithAllocObject () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = new CallVirtualFromConstructorDerived (42); - Assert.IsTrue ( - t.Called, - "CalledFromConstructor method override should have been called."); - Assert.IsFalse ( - t.InvokedActivationConstructor, - "Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.IsNull (acIntermediate, - "Activation Constructor should not have been called, because of AllocObject semantics"); - Assert.AreSame (t, cfIntermediate, - "Expected t and cfIntermediate to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - - static void Dispose (ref T peer) - where T : class, IJavaPeerable - { - if (peer == null) - return; - - peer.Dispose (); - peer = null; - } - - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateManagedInstanceFirst_WithNewObject () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = new CallVirtualFromConstructorDerived (42, useNewObject: true); - Assert.IsFalse ( - t.Called, - "CalledFromConstructor method override was called on a different instance."); - Assert.IsFalse ( - t.InvokedActivationConstructor, - "Activation Constructor should not have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.IsNotNull (acIntermediate, - "Activation Constructor should have been called, because of NewObject"); - Assert.IsTrue ( - acIntermediate.Called, - "CalledFromConstructor method override should have been called on acIntermediate."); - Assert.IsTrue ( - acIntermediate.InvokedActivationConstructor, - "Activation Constructor should have been called on intermediate instance, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.AreNotSame (t, acIntermediate, - "Expected t and registered to be different instances; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}"); - Assert.AreNotSame (t, cfIntermediate, - "Expected t and cfIntermediate to be different instances; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - Assert.AreSame (acIntermediate, cfIntermediate, - "Expected acIntermediate and cfIntermediate to be the same instance; " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - - [Test] - [Category ("NativeAOTIgnore")] // https://github.com/dotnet/android/issues/10950 - public void CreateJavaInstanceFirst () - { - CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null; - CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null; - - using var t = CallVirtualFromConstructorDerived.NewInstance (42); - - Assert.IsTrue ( - t.Called, - "CalledFromConstructor method override should have been called."); - Assert.IsTrue ( - t.InvokedActivationConstructor, - "Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct()."); - Assert.IsTrue ( - t.InvokedConstructor, - "(int) constructor should have been called, via ManagedPeer.construct()."); - - var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference); - var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor; - var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor; - - Assert.AreSame (t, registered, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}"); - Assert.AreSame (t, acIntermediate, - "Expected t and registered to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}"); - Assert.AreSame (t, cfIntermediate, - "Expected t and cfIntermediate to be the same instance; " + - $"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " + - $"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}"); - - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor); - Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs deleted file mode 100644 index 79b64ef2fd5..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] - public class JavaManagedGCBridgeTests : JavaVMFixture { - -#if !NO_GC_BRIDGE_SUPPORT - // https://github.com/mono/mono/blob/98d2314/mono/tests/sgen-bridge-xref.cs - [Test] - public void CrossReferences () - { - using (var array = new JavaObjectArray (2)) { - WeakReference root = null, child = null; - var t = new Thread (() => SetupLinks (array, out root, out child)); - t.Start (); - t.Join (); - - JniEnvironment.Runtime.ValueManager.CollectPeers (); - CrossReferenceBridge a, b; - a = b = null; - Console.WriteLine ("try get A {0}", root.TryGetTarget (out a)); - Console.WriteLine ("try get B {0}", child.TryGetTarget (out b)); - Console.WriteLine ("a is null {0}", a == null); - Console.WriteLine ("b is null {0}", b == null); - - Assert.IsNotNull (a); - Assert.IsNotNull (b); - } - } -#endif // !NO_GC_BRIDGE_SUPPORT - - static void SetupLinks (JavaObjectArray array, out WeakReference root, out WeakReference child) - { - var a = new CrossReferenceBridge () { - id = "bridge", - }; - var b = new CrossReferenceBridge () { - id = "child", - }; - a.link.Add (b); - - array [0] = a; - array [1] = b; - - root = new WeakReference (a, true); - child = new WeakReference (b, true); - } - } - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - public class CrossReferenceBridge : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/CrossReferenceBridge"; - - public string id; - public List link = new List (); - - protected override void Dispose (bool disposing) - { - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs index e5d2bc2fa98..c352abdf3f5 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectArrayTest.cs @@ -165,40 +165,4 @@ public void ObjectArrayType () } } - [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] - public class JavaObjectArray_object_ContractTest : JavaObjectArrayContractTest { - static readonly object a = new object (); - - protected override object CreateValueA () {return a;} - protected override object CreateValueB () {return 42;} - - int grefStartCount; - - [OneTimeSetUp] - public void BeginCheckGlobalRefCount () - { - // So that the JavaProxyObject.TypeRef GREF isn't counted. - using (var o = new JavaObjectArray (1)) - o [0] = a; - grefStartCount = JniEnvironment.Runtime.GlobalReferenceCount; - } - - [OneTimeTearDown] - public void EndCheckGlobalRefCount () - { - int gref = JniEnvironment.Runtime.GlobalReferenceCount; - Assert.IsTrue (gref <= (grefStartCount), - string.Format ("JNI global references: grefStartCount={0}; gref={1}", grefStartCount, gref)); - JniEnvironment.Runtime.ValueManager.CollectPeers (); - } - - [Test] - public void ObjectArrayType () - { - var c = CreateCollection (new object[0]); - Assert.AreEqual ("[Ljava/lang/Object;", ((IJavaPeerable) c).GetJniTypeName ()); - Dispose (c); - } - } } diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs index 1d308fc945a..49bd8b16dd0 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs @@ -187,13 +187,7 @@ public void Ctor_Exceptions () #if __ANDROID__ Assert.Throws (() => new JavaObjectWithMissingJavaPeer ()).Dispose (); -#else // !__ANDROID__ - // Note: `JavaObjectWithNoJavaPeer` creation works on Android because tooling provides all - // typemap entries. On desktop, we use the hardcoded dictionary in JavaVMFixture, which - // deliberately *lacks* an entry for `JavaObjectWithNoJavaPeer`. - Assert.Throws (() => new JavaObjectWithNoJavaPeer ()); - Assert.Throws (() => new JavaObjectWithMissingJavaPeer ()).Dispose (); -#endif // !__ANDROID__ +#endif // __ANDROID__ } [Test] @@ -224,11 +218,6 @@ public void DisposeAccessesThis () } } -#if !__ANDROID__ - class JavaObjectWithNoJavaPeer : JavaObject { - } -#endif // !__ANDROID__ - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] class JavaObjectWithMissingJavaPeer : JavaObject { internal const string JniTypeName = "__this__/__type__/__had__/__better__/__not__/__Exist__"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs index 48e6580cd8c..367a8e050cd 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaPeerableExtensionsTests.cs @@ -23,10 +23,6 @@ public void JavaAs_Exceptions () using var o = new JavaObject (ref r, JniObjectReferenceOptions.Copy); // MyJavaInterfaceImpl doesn't provide an activation constructor Assert.Throws(() => o.JavaAs()); -#if !__ANDROID__ - // JavaObjectWithNoJavaPeer has no Java peer - Assert.Throws(() => v.JavaAs()); -#endif // !__ANDROID__ } [Test] diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index 356468bb39a..801f7cc8dc7 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; -using System.Threading; using Java.Interop; @@ -48,12 +47,6 @@ enum ReplacementMethodStorage { [TestType.JniTypeName] = typeof (TestType), #endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT [GenericHolder.JniTypeName] = typeof (GenericHolder<>), - [RenameClassBase.JniTypeName] = typeof (RenameClassBase), - [RenameClassDerived.JniTypeName] = typeof (RenameClassDerived), - [AnotherJavaInterfaceImpl.JniTypeName] = typeof (AnotherJavaInterfaceImpl), - [CallVirtualFromConstructorBase.JniTypeName] = typeof (CallVirtualFromConstructorBase), - [CallVirtualFromConstructorDerived.JniTypeName] = typeof (CallVirtualFromConstructorDerived), - [CrossReferenceBridge.JniTypeName] = typeof (CrossReferenceBridge), [GetThis.JniTypeName] = typeof (GetThis), [IAndroidInterface.JniTypeName] = typeof (IAndroidInterface), [IJavaInterface.JniTypeName] = typeof (IJavaInterface), @@ -115,10 +108,8 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) } } - public string? RequestedFallbackTypesForSimpleReference; protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) { - RequestedFallbackTypesForSimpleReference = jniSimpleReference; Debug.WriteLine ($"# GetStaticMethodFallbackTypes (jniSimpleReference={jniSimpleReference})"); var slash = jniSimpleReference.LastIndexOf ('/'); @@ -136,29 +127,12 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) } Dictionary ReplacmentTypes = new() { - ["net/dot/jni/test/RenameClassBase1"] = "net/dot/jni/test/RenameClassBase2", [FieldRemapBase.JniTypeName] = FieldRemapBase.RuntimeJniTypeName, [FieldRemapBase.RuntimeJniTypeName] = FieldRemapBase.FinalJniTypeName, }; - string? trackedReplacementType; - int replacementTypeStringLookupCount; - int replacementTypeUtf8LookupCount; - - public void TrackReplacementTypeLookups (string jniSimpleReference) - { - trackedReplacementType = jniSimpleReference; - replacementTypeStringLookupCount = 0; - replacementTypeUtf8LookupCount = 0; - } - - public (int String, int Utf8) GetReplacementTypeLookupCounts () - => (replacementTypeStringLookupCount, replacementTypeUtf8LookupCount); - protected override string? GetReplacementTypeCore (string jniSimpleReference) { - if (jniSimpleReference == trackedReplacementType) - Interlocked.Increment (ref replacementTypeStringLookupCount); return ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) ? value : null; @@ -166,8 +140,6 @@ public void TrackReplacementTypeLookups (string jniSimpleReference) protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string? replacement, out IntPtr replacementUtf8) { - if (jniSimpleReference == trackedReplacementType) - Interlocked.Increment (ref replacementTypeUtf8LookupCount); replacement = null; replacementUtf8 = ReplacmentTypes.TryGetValue (jniSimpleReference, out var value) ? GetUtf8Value (value) @@ -181,10 +153,6 @@ protected override void GetReplacementTypeInfoCore (string jniSimpleReference, o [("java/lang/Object", "remappedStaticAbs", "(I)I")] = ("java/lang/Math", "abs", null, null, false, ReplacementMethodStorage.Strings), [("java/lang/Runtime", "remappedToGetRuntime", null)] = (null, "getRuntime", null, null, false, ReplacementMethodStorage.Strings), - // NOTE: key must use *post-renamed* value, not pre-renamed value - // NOTE: SourceSignature lacking return type; "closer in spirit" to what `remapping-config.json` allows - [("net/dot/jni/test/RenameClassBase2", "hashCode", "()")] = ("net/dot/jni/test/RenameClassBase2", "myNewHashCode", null, null, false, ReplacementMethodStorage.TypeUtf8 | ReplacementMethodStorage.MethodUtf8), - // Renamed parameter types: the target descriptor is pinned explicitly, which is what // `target-method-signature` carries. [("java/lang/StringBuilder", "", "(Lnet/dot/jni/test/RenamedInt;)V")] = (null, "", "(I)V", null, false, ReplacementMethodStorage.Strings), diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs deleted file mode 100644 index bce118ca8e7..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniInstanceMethodIDTest.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests -{ - [TestFixture] - public class JniInstanceMethodIDTest : JavaVMFixture - { - // https://code.google.com/p/android/issues/detail?id=65710 - [Test] - [Category ("TrimmableTypeMapUnsupported")] - public unsafe void CallNonvirtualVoidMethod_WithBaseMethodIDAndDerivedType () - { - using (var b = new JniType ("net/dot/jni/test/CallNonvirtualBase")) - using (var d = new JniType ("net/dot/jni/test/CallNonvirtualDerived")) { - var m = b.GetInstanceMethod ("method", "()V"); - var f = b.GetInstanceField ("methodInvoked", "Z"); - - var c = d.GetConstructor ("()V"); - var g = d.GetInstanceField ("methodInvoked", "Z"); - var o = d.NewObject (c, null); - try { - if (JavaVMFixture.CallNonvirtualVoidMethodSupportsDeclaringClassMismatch) { - JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod (o, d.PeerReference, m); - Assert.IsFalse (JniEnvironment.InstanceFields.GetBooleanField (o, f)); - Assert.IsTrue (JniEnvironment.InstanceFields.GetBooleanField (o, g)); - } else { - JniEnvironment.InstanceMethods.CallNonvirtualVoidMethod (o, d.PeerReference, m); - Assert.IsTrue (JniEnvironment.InstanceFields.GetBooleanField (o, f)); - Assert.IsFalse (JniEnvironment.InstanceFields.GetBooleanField (o, g)); - } - } finally { - JniObjectReference.Dispose (ref o); - } - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs index 83490b0c6e5..ad1f4ffd85c 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniPeerMembersTests.cs @@ -18,62 +18,6 @@ public void Ctor_CanReferenceNonexistentType () JniPeerMembers.Dispose (members); } -#if !ANDROID // Android doesn't allow providing a custom TypeManager - [Test] - [NonParallelizable] - public void HandledReplacementTypeMissDoesNotUseStringFallback () - { - var typeManager = JavaVMFixture.TypeManager; - Assert.IsNotNull (typeManager); - typeManager.TrackReplacementTypeLookups ("java/lang/Double"); - try { - var members = new JniPeerMembers ("java/lang/Double", typeof (MyString)); - JniPeerMembers.Dispose (members); - - var counts = typeManager.GetReplacementTypeLookupCounts (); - Assert.AreEqual (1, counts.Utf8); - Assert.AreEqual (0, counts.String); - } finally { - typeManager.TrackReplacementTypeLookups (""); - } - } -#endif // !ANDROID - - [Test] - [Category ("TrimmableTypeMapUnsupported")] - public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod () - { - Assert.IsNull (GetInstanceMethods (MyString._members.InstanceMethods)); - using (var s = new MyString ("hello!")) { - var registered = GetInstanceMethods (MyString._members.InstanceMethods); - Assert.AreEqual (1, registered.Count); // for the constructor - Assert.AreEqual ("hello!", s.ToString ()); - Assert.AreEqual (1, registered.Count); - } - } - - [Test] - [Category ("TrimmableTypeMapUnsupported")] - public void ConcurrentFirstUsePublishesSingleInstanceMethodCache () - { - var members = new JniPeerMembers (MyString.JniTypeName, typeof (MyString)); - try { - var methods = members.InstanceMethods; - var constructors = new JniMethodInfo [16]; - - Assert.IsNull (GetInstanceMethods (methods)); - Parallel.For (0, constructors.Length, i => constructors [i] = methods.GetConstructor ("()V")); - - var registered = GetInstanceMethods (methods); - Assert.AreEqual (1, registered.Count); - foreach (var constructor in constructors) - Assert.AreSame (constructors [0], constructor); - Assert.AreSame (registered ["()V"], constructors [0]); - } finally { - JniPeerMembers.Dispose (members); - } - } - [Test] public void PeerMemberCachesAreInitiallyNull () { @@ -225,10 +169,6 @@ public void MethodLookupForNonexistentStaticMethodWillTryFallbacks () // --- End of managed Java.Lang.NoSuchMethodError stack trace --- // ``` Assert.IsTrue (e.Message.Contains ("doesNotExist", StringComparison.Ordinal)); -#if !ANDROID // Android doesn't allow providing a custom TypeManager - Assert.AreEqual ("java/lang/Runtime", - JavaVMFixture.TypeManager.RequestedFallbackTypesForSimpleReference); -#endif // !ANDROID } } @@ -505,16 +445,6 @@ public void ReplacementMethodUsesTargetSignature () } #endif // !__ANDROID__ - [Test] - [Category ("NativeAOTIgnore")] - [Category ("TrimmableTypeMapUnsupported")] - public void ReplacementTypeUsedForMethodLookup () - { - using var o = new RenameClassDerived (); - int r = o.hashCode(); - Assert.AreEqual (33, r); - } - [Test] [Category ("NativeAOTIgnore")] public void ReplaceInstanceMethodName () @@ -552,54 +482,6 @@ public void ReplaceInstanceMethodWithStaticMethod () o.remappedToStaticHashCode (); } -#if !__ANDROID__ - // Note: this test looks up a static method from one class, then - // calls `JNIEnv::CallStaticObjectMethod()` passing in a jclass - // for a *different* class. - // - // This appears to work on Desktop JVM. - // - // On Android, this will ABORT the app: - // JNI DETECTED ERROR IN APPLICATION: can't call static int com.xamarin.interop.DesugarAndroidInterface$_CC.getClassName() with class java.lang.Class - // in call to CallStaticObjectMethodA - // - // This *also* aborts on JDK-17 + macOS + arm64: - // FATAL ERROR in native method: Wrong object class or methodID passed to JNI call - // Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code) - // V [libjvm.dylib+0x4f718c] jniCheck::validate_call(JavaThread*, _jclass*, _jmethodID*, _jobject*)+0x98 - // V [libjvm.dylib+0x506474] checked_jni_CallStaticObjectMethodA+0x150 - // C [native JNI wrapper] java_interop_jnienv_call_static_object_method_a+0x48 - // C 0x000000010798a8d8 - // C 0x000000010798a540 - // C 0x000000010798fa94 - // C [libcoreclr.dylib+0x2db774] CallDescrWorkerInternal+0x84 - // C [libcoreclr.dylib+0x150db4] CallDescrWorkerWithHandler(CallDescrData*, int)+0x74 - // C [libcoreclr.dylib+0x1f92d0] RuntimeMethodHandle::InvokeMethod(Object*, void**, SignatureNative*, bool)+0x79c - // C 0x0000000104020ce4 - // … - // - // *Fascinating* the differences that can appear between JVM implementations - [Test] - public unsafe void DoesTheJmethodNeedToMatchDeclaringType () - { - if (Environment.GetEnvironmentVariable ("CPUTYPE") is string cpu && cpu == "arm64") { - Assert.Ignore ("nope!"); - } - if (VM.JdkInfo.Version is Version v && v >= new Version (17, 0)) { - // JDK-17 now crashes on this test as well: - // FATAL ERROR in native method: Wrong object class or methodID passed to JNI call - Assert.Ignore ("nope!"); - } - var iface = new JniType ("net/dot/jni/test/AndroidInterface"); - var desugar = new JniType ("net/dot/jni/test/DesugarAndroidInterface$_CC"); - var m = desugar.GetStaticMethod ("getClassName", "()Ljava/lang/String;"); - - var r = JniEnvironment.StaticMethods.CallStaticObjectMethod (iface.PeerReference, m, null); - var s = JniEnvironment.Strings.ToString (ref r, JniObjectReferenceOptions.CopyAndDispose); - Assert.AreEqual ("DesugarAndroidInterface$-CC", s); - } -#endif // !__ANDROID__ - [Test] public void DesugarInterfaceStaticMethod () { @@ -613,27 +495,6 @@ abstract class JavaLangSystemTestObject : JavaObject { internal const string JniTypeName = "java/lang/System"; } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class MyString : JavaObject { - internal const string JniTypeName = "java/lang/String"; - - internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (MyString)); - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - public unsafe MyString (string value) - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - const string id = "(Ljava/lang/String;)V"; - var peer = _members.InstanceMethods.StartGenericCreateInstance (id, GetType (), value); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - _members.InstanceMethods.FinishGenericCreateInstance (id, this, value); - } - } - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] class JavaLangRemappingTestObject : JavaObject { internal const string JniTypeName = "java/lang/Object"; @@ -730,37 +591,6 @@ public static unsafe void doesNotExist () } } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class RenameClassBase : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/RenameClassBase1"; - static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (RenameClassBase)); - - public override JniPeerMembers JniPeerMembers => _members; - - public RenameClassBase () - { - } - - public virtual unsafe int hashCode () - { - const string id = "hashCode.()I"; - return _members.InstanceMethods.InvokeVirtualInt32Method (id, this, null); - } - } - - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - class RenameClassDerived : RenameClassBase { - internal new const string JniTypeName = "net/dot/jni/test/RenameClassDerived"; - public RenameClassDerived () - { - } - - public override unsafe int hashCode () - { - return base.hashCode (); - } - } - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] interface IAndroidInterface : IJavaPeerable { internal const string JniTypeName = "net/dot/jni/test/AndroidInterface"; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs deleted file mode 100644 index f2dd684a8dd..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniTypeManagerTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - public class JniRuntimeJniTypeManagerTests : JavaVMFixture { - - [Test] - public void ReplacementMethodInfoToStringConvertsUtf8Pointers () - { - var type = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); - var name = Marshal.StringToCoTaskMemUTF8 ("toString"); - var signature = Marshal.StringToCoTaskMemUTF8 ("()Ljava/lang/String;"); - try { - var info = new JniRuntime.ReplacementMethodInfo { - TargetJniTypeUtf8 = type, - TargetJniMethodNameUtf8 = name, - TargetJniMethodSignatureUtf8 = signature, - }; - - var value = info.ToString (); - - Assert.That (value, Does.Contain ("TargetJniTypeUtf8 = \"java/lang/Object\"")); - Assert.That (value, Does.Contain ("TargetJniMethodNameUtf8 = \"toString\"")); - Assert.That (value, Does.Contain ("TargetJniMethodSignatureUtf8 = \"()Ljava/lang/String;\"")); - } finally { - Marshal.ZeroFreeCoTaskMemUTF8 (type); - Marshal.ZeroFreeCoTaskMemUTF8 (name); - Marshal.ZeroFreeCoTaskMemUTF8 (signature); - } - } - - [Test] - public void ReplacementTypeInfoSupportsStringAndUtf8Results () - { - using var stringManager = new StringReplacementTypeManager (); - stringManager.GetReplacementTypeInfo ("java/lang/String", out var stringReplacement, out var stringReplacementUtf8); - Assert.AreEqual ("java/lang/Object", stringReplacement); - Assert.AreEqual (IntPtr.Zero, stringReplacementUtf8); - Assert.AreEqual (1, stringManager.LookupCount); - - var utf8Value = Marshal.StringToCoTaskMemUTF8 ("java/lang/Object"); - try { - using var utf8Manager = new Utf8ReplacementTypeManager (utf8Value); - utf8Manager.GetReplacementTypeInfo ("java/lang/Double", out var missingReplacement, out var missingReplacementUtf8); - Assert.IsNull (missingReplacement); - Assert.AreEqual (IntPtr.Zero, missingReplacementUtf8); - utf8Manager.GetReplacementTypeInfo ("java/lang/String", out var replacement, out var replacementUtf8); - Assert.IsNull (replacement); - Assert.AreEqual (utf8Value, replacementUtf8); - Assert.AreEqual ("java/lang/Object", utf8Manager.GetReplacementType ("java/lang/String")); - } finally { - Marshal.ZeroFreeCoTaskMemUTF8 (utf8Value); - } - } - - [Test] - [Category ("TrimmableTypeMapUnsupported")] - [RequiresDynamicCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")] - public void GetInvokerType () - { - using (var vm = new MyTypeManager ()) { - // Concrete type; no invoker - Assert.IsNull (vm.GetInvokerType (typeof (JavaObject))); - - // Not a bound abstract Java type; no invoker - Assert.IsNull (vm.GetInvokerType (typeof (System.ICloneable))); - - // Bound abstract Java type; has an invoker - Assert.AreSame (typeof (IJavaInterfaceInvoker), vm.GetInvokerType (typeof (IJavaInterface))); - } - } - - [RequiresDynamicCode ("MyTypeManager uses ReflectionJniTypeManager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("MyTypeManager uses ReflectionJniTypeManager, which is reflection-based and not trimming-compatible.")] - class MyTypeManager : JniRuntime.ReflectionJniTypeManager { - public MyTypeManager () - { - } - } - - class StringReplacementTypeManager : JniRuntime.JniTypeManager { - - public int LookupCount { get; private set; } - - protected override string GetReplacementTypeCore (string jniSimpleReference) - { - LookupCount++; - return jniSimpleReference == "java/lang/String" ? "java/lang/Object" : null; - } - } - - class Utf8ReplacementTypeManager : JniRuntime.JniTypeManager { - - readonly IntPtr replacement; - - public Utf8ReplacementTypeManager (IntPtr replacement) - { - this.replacement = replacement; - } - - protected override void GetReplacementTypeInfoCore (string jniSimpleReference, out string replacement, out IntPtr replacementUtf8) - { - replacement = jniSimpleReference == "java/lang/String" ? "ignored/string/value" : null; - replacementUtf8 = jniSimpleReference == "java/lang/String" ? this.replacement : IntPtr.Zero; - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs deleted file mode 100644 index a712105d7be..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Collections.Generic; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [TestFixture] - public class JniRuntimeJniValueManagerTests : JavaVMFixture { - - [Test] - public void CreateValue () - { - using (var vm = new MyValueManager ()) - using (var o = new JavaObject ()) { - vm.OnSetRuntime (JniRuntime.CurrentRuntime); - - var r = o.PeerReference; - var x = (IJavaPeerable) vm.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x.Dispose (); - - x = vm.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x.Dispose (); - } - } - - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] - class MyValueManager : JniRuntime.ReflectionJniValueManager { - - public override void WaitForGCBridgeProcessing () - { - } - - public override void CollectPeers () - { - } - - public override void AddPeer (IJavaPeerable reference) - { - } - - public override void RemovePeer (IJavaPeerable reference) - { - } - - public override void FinalizePeer (IJavaPeerable reference) - { - } - - public override List GetSurfacedPeers () - { - return null; - } - - public override IJavaPeerable PeekPeer (JniObjectReference reference) - { - return null; - } - - } - - [Test] - public void GetValue_ReturnsAlias () - { - var local = new JavaObject (); - local.UnregisterFromRuntime (); - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (local.PeerReference)); - // GetObject must always return a value (unless handle is null, etc.). - // However, since we called local.UnregisterFromRuntime(), - // JniRuntime.PeekObject() is null (asserted above), but GetObject() must - // **still** return _something_. - // In this case, it returns an _alias_. - // TODO: "most derived type" alias generation. (Not relevant here, but...) - var p = local.PeerReference; - var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (local, alias); - alias.Dispose (); - local.Dispose (); - } - - [Test] - public void GetValue_ReturnsNullWithNullHandle () - { - var r = new JniObjectReference (); - var o = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsNull (o); - } - - [Test] - public void PeekValue () - { - JniObjectReference lref; - using (var o = new JavaObject ()) { - lref = o.PeerReference.NewLocalRef (); - Assert.AreSame (o, JniRuntime.CurrentRuntime.ValueManager.PeekValue (lref)); - } - // At this point, the Java-side object is kept alive by `lref`, - // but the wrapper instance has been disposed, and thus should - // be unregistered, and thus unfindable. - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (lref)); - JniObjectReference.Dispose (ref lref); - } - - public void PeekValue_BoxedObjects () - { - var vm = JniRuntime.CurrentRuntime.ValueManager; - var marshaler = vm.GetValueMarshaler (); - var ad = AppDomain.CurrentDomain; - - var proxy = marshaler.CreateGenericArgumentState (ad); - Assert.AreSame (ad, vm.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ad, ref proxy); - - var ex = new InvalidOperationException ("boo!"); - proxy = marshaler.CreateGenericArgumentState (ex); - Assert.AreSame (ex, vm.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ex, ref proxy); - } - - [Test] - public void GetValue_ReturnsNullWithInvalidSafeHandle () - { - var invalid = new JniObjectReference (); - Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose)); - } - -#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - [Test] - public unsafe void GetValue_FindBestMatchType () - { - using (var t = new JniType (TestType.JniTypeName)) { - var c = t.GetConstructor ("()V"); - var o = t.NewObject (c, null); - using (var w = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) { - Assert.AreEqual (typeof (TestType), w.GetType ()); - Assert.IsTrue (((TestType) w).ExecutedActivationConstructor); - } - } - } -#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs deleted file mode 100644 index a3343de62a0..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs +++ /dev/null @@ -1,535 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests { - - // Android doesn't support `[NonParallelizable]`, but runs tests sequentially by default. -#if !__ANDROID__ - // Modifies JniRuntime.valueManager instance field; can't be done in parallel - [NonParallelizable] -#endif // !__ANDROID__ - [Category ("TrimmableTypeMapUnsupported")] - public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture { - - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - protected abstract Type ValueManagerType { - get; - } - - protected virtual JniRuntime.JniValueManager CreateValueManager () - { - var manager = Activator.CreateInstance (ValueManagerType) as JniRuntime.JniValueManager; - return manager ?? throw new InvalidOperationException ($"Could not create instance of `{ValueManagerType}`!"); - } - -#pragma warning disable CS8618 - JniRuntime.JniValueManager systemManager; - JniRuntime.JniValueManager valueManager; -#pragma warning restore CS8618 - - [SetUp] - public void CreateVM () - { - systemManager = JniRuntime.CurrentRuntime.valueManager!; - valueManager = CreateValueManager (); - valueManager.OnSetRuntime (JniRuntime.CurrentRuntime); - JniRuntime.CurrentRuntime.valueManager = valueManager; - } - - [TearDown] - public void DestroyVM () - { - JniRuntime.CurrentRuntime.valueManager = systemManager; - systemManager = null!; - valueManager?.Dispose (); - valueManager = null!; - } - - [Test] - public void AddPeer () - { - } - - int GetSurfacedPeersCount () - { - return valueManager.GetSurfacedPeers ().Count; - } - - [Test] - public void AddPeer_NoDuplicates () - { - int startPeerCount = GetSurfacedPeersCount (); - using (var v = new MyDisposableObject ()) { - // MyDisposableObject ctor implicitly calls AddPeer(); - Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ()); - valueManager.AddPeer (v); - Assert.AreEqual (startPeerCount + 1, GetSurfacedPeersCount (), DumpPeers ()); - } - } - - [Test] - public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers () - { - int startPeerCount = GetSurfacedPeersCount (); - - var g = new GetThis (); - var surfaced = valueManager.GetSurfacedPeers (); - Assert.AreEqual (startPeerCount + 1, surfaced.Count); - - var found = false; - foreach (var pr in surfaced) { - if (!pr.SurfacedPeer.TryGetTarget (out var p)) - continue; - if (object.ReferenceEquals (g, p)) { - found = true; - } - } - Assert.IsTrue (found); - - var localRef = g.PeerReference.NewLocalRef (); - g.Dispose (); - Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ()); - Assert.IsNull (valueManager.PeekPeer (localRef)); - JniObjectReference.Dispose (ref localRef); - } - - [Test] - public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers () - { - int startPeerCount = GetSurfacedPeersCount (); - - var g = new GetThis (); - var surfaced = valueManager.GetSurfacedPeers (); - Assert.AreEqual (startPeerCount + 1, surfaced.Count); - - var found = false; - foreach (var pr in surfaced) { - if (!pr.SurfacedPeer.TryGetTarget (out var p)) - continue; - if (object.ReferenceEquals (g, p)) { - found = true; - } - } - Assert.IsTrue (found); - - var localRef = g.PeerReference.NewLocalRef (); - g.Dispose (); - Assert.AreEqual (startPeerCount, GetSurfacedPeersCount ()); - Assert.IsNull (valueManager.PeekPeer (localRef)); - JniObjectReference.Dispose (ref localRef); - } - - // https://github.com/dotnet/android/issues/11101 - [Test] - public void ConstructPeer_CalledMultipleTimes_ShouldNotLeakGlobalRefs () - { - // Simulate what TypeManager.Activate does in dotnet/android: - // 1. Create an uninitialized JavaObject - // 2. Set a peer reference (storing the raw handle) - // 3. The constructor chain then calls ConstructPeer multiple times - // - // This should not leak JNI global references. - - using (var original = new MyDisposableObject ()) { - // Get the jobject handle, simulating the IntPtr jobject param in Activate - var handle = original.PeerReference; - - // Create an uninitialized peer and set its reference (simulating Activate) - var peer = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (typeof (MyDisposableObject)); - peer.SetPeerReference (new JniObjectReference (handle.Handle)); - - try { - // Now simulate the constructor chain calling ConstructPeer multiple times - var ref1 = new JniObjectReference (handle.Handle); - valueManager.ConstructPeer (peer, ref ref1, JniObjectReferenceOptions.Copy); - - int grefAfterFirst = JniEnvironment.Runtime.GlobalReferenceCount; - - var ref2 = new JniObjectReference (handle.Handle); - valueManager.ConstructPeer (peer, ref ref2, JniObjectReferenceOptions.Copy); - - int grefAfterSecond = JniEnvironment.Runtime.GlobalReferenceCount; - - // The second ConstructPeer should NOT create an additional global ref - Assert.AreEqual (grefAfterFirst, grefAfterSecond, - "Second ConstructPeer call should not create an additional global ref"); - } finally { - peer.Dispose (); - } - } - } - - - [Test] - public void CollectPeers () - { - // TODO - } - - [Test] - public void CreatePeer_InvalidHandleReturnsNull () - { - var r = new JniObjectReference (); - var o = valueManager.CreatePeer (ref r, JniObjectReferenceOptions.Copy, null); - Assert.IsNull (o); - } - - [Test] - public unsafe void CreatePeer_UsesFallbackType () - { - using var t = new JniType (AnotherJavaInterfaceImpl.JniTypeName); - - var ctor = t.GetConstructor ("()V"); - var lref = t.NewObject (ctor, null); - - using var p = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (IJavaInterface)); - - Assert.IsFalse (lref.IsValid); // .CopyAndDispose disposes - - Assert.IsNotNull (p); - Assert.AreSame (typeof (IJavaInterfaceInvoker), p!.GetType ()); - } - - [Test] - public void CreatePeer_CreatesNewValueUsingActivationConstructor () - { - using var v1 = new AnotherJavaInterfaceImpl (); - var lref = v1.PeerReference.NewLocalRef (); - try { - using var v2 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (AnotherJavaInterfaceImpl)); - Assert.AreNotSame (v1, v2, "CreatePeer() should create new values"); - } - finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void DisposePeerUnlessReferenced_CallsManagedDispose () - { - using var registered = new AnotherJavaInterfaceImpl (); - var lref = registered.PeerReference.NewLocalRef (); - try { - using var alias = valueManager.CreatePeer ( - ref lref, - JniObjectReferenceOptions.Copy, - typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl - ?? throw new InvalidOperationException ("Could not create peer alias."); - - Assert.AreNotSame (registered, alias); - Assert.AreSame (registered, valueManager.PeekPeer (lref)); - - alias.DisposeUnlessReferenced (); - - Assert.IsTrue (alias.DisposeCalled, "The unregistered alias should receive its managed disposal callback."); - Assert.IsFalse (alias.PeerReference.IsValid, "The unregistered alias should release its JNI reference."); - Assert.IsFalse (registered.DisposeCalled, "The registered peer should remain undisposed."); - Assert.IsTrue (registered.PeerReference.IsValid, "The registered peer should retain its JNI reference."); - } finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public unsafe void GetPeer_ReentrantActivation_ReturnsRegisteredPeer () - { - using var t = new JniType (AnotherJavaInterfaceImpl.JniTypeName); - var ctor = t.GetConstructor ("()V"); - var lref = t.NewObject (ctor, null); - AnotherJavaInterfaceImpl? reentrant = null; - var createdPeers = new List (); - try { - Assert.IsNull (valueManager.PeekPeer (lref)); - AnotherJavaInterfaceImpl.PeerCreated = createdPeers.Add; - AnotherJavaInterfaceImpl.BeforeConstruct = reference => { - reentrant = valueManager.GetPeer (reference, typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl; - }; - - var peer = valueManager.GetPeer (lref, typeof (AnotherJavaInterfaceImpl)) as AnotherJavaInterfaceImpl - ?? throw new InvalidOperationException ("Could not create peer."); - var registered = valueManager.PeekPeer (lref) - ?? throw new InvalidOperationException ("No peer was registered."); - - Assert.AreSame (reentrant, registered, "The reentrant lookup should register first."); - Assert.AreSame (registered, peer, "The outer lookup should return the registered peer."); - Assert.AreEqual (2, createdPeers.Count, "Both lookups should construct a peer."); - Assert.AreNotSame (createdPeers [0], createdPeers [1]); - foreach (var created in createdPeers) { - Assert.AreEqual (ReferenceEquals (created, registered), created.PeerReference.IsValid, - "Only the returned registered peer should retain a JNI reference."); - Assert.AreEqual (!ReferenceEquals (created, registered), created.DisposeCalled, - "Only the losing alias should receive its managed disposal callback."); - } - } finally { - AnotherJavaInterfaceImpl.BeforeConstruct = null; - AnotherJavaInterfaceImpl.PeerCreated = null; - foreach (var peer in createdPeers) - peer.Dispose (); - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void CreatePeer_ThrowsIfNoActivationConstructorPresent () - { - using var v1 = new GetThis (); - var lref = v1.PeerReference.NewLocalRef (); - var ex = Assert.Throws ( - () => valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.CopyAndDispose, typeof (GetThis)), - $"`GetThis` has no activation constructor, so attempting to use it should throw NotSupportedException."); - Assert.IsTrue (lref.IsValid, "lref should still be valid"); - JniObjectReference.Dispose (ref lref); - } - - [Test] - public void CreatePeer_ReplaceableDoesNotReplace () - { - var v = new AnotherJavaInterfaceImpl (); - var lref = v.PeerReference.NewLocalRef (); - v.Dispose (); - - try { - Assert.IsNull (valueManager.PeekPeer (lref), "v.Dispose() should have unregistered the peer."); - var peer1 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.Copy, typeof (AnotherJavaInterfaceImpl)); - Assert.IsTrue ( - peer1!.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable), - $"Expected peer1.JniManagedPeerState to have .Replaceable, but was {peer1.JniManagedPeerState}."); - Assert.AreSame (peer1, valueManager.PeekPeer (lref), - $"Expected peer1==PeekValue(peer1.PeerReference); it's the only one that should exist!"); - - var peer2 = valueManager.CreatePeer (ref lref, JniObjectReferenceOptions.Copy, typeof (AnotherJavaInterfaceImpl)); - Assert.IsTrue ( - peer2!.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable), - $"Expected peer2.JniManagedPeerState to have .Replaceable, but was {peer2.JniManagedPeerState}."); - Assert.AreNotSame (peer1, peer2, "Expected peer1 and peer2 to be different instances."); - - var peeked = valueManager.PeekPeer (lref); - Assert.AreSame (peer1, peeked, - "Expected peer1 and peeked to be the same instance; " + - $"peeked={RuntimeHelpers.GetHashCode (peeked).ToString ("x")}, " + - $"peer1={RuntimeHelpers.GetHashCode (peer1).ToString ("x")}, " + - $"peer2={RuntimeHelpers.GetHashCode (peer2).ToString ("x")}"); - } finally { - JniObjectReference.Dispose (ref lref); - } - } - - [Test] - public void CreateValue () - { - using (var o = new JavaObject ()) { - var r = o.PeerReference; - var x = (IJavaPeerable) valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy)!; - Assert.AreNotSame (o, x); - x.Dispose (); - - x = valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (o, x); - x!.Dispose (); - } - } - - [Test] - public void GetValue_ReturnsAlias () - { - var local = new JavaObject (); - local.UnregisterFromRuntime (); - Assert.IsNull (valueManager.PeekValue (local.PeerReference)); - // GetObject must always return a value (unless handle is null, etc.). - // However, since we called local.UnregisterFromRuntime(), - // JniRuntime.PeekObject() is null (asserted above), but GetObject() must - // **still** return _something_. - // In this case, it returns an _alias_. - // TODO: "most derived type" alias generation. (Not relevant here, but...) - var p = local.PeerReference; - var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy); - Assert.AreNotSame (local, alias); - alias!.Dispose (); - local.Dispose (); - } - - [Test] - public void GetValue_ReturnsNullWithNullHandle () - { - var r = new JniObjectReference (); - var o = valueManager.GetValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsNull (o); - } - - [Test] - public void GetValue_ReturnsNullWithInvalidSafeHandle () - { - var invalid = new JniObjectReference (); - Assert.IsNull (valueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose)); - } - - [Test] - public unsafe void GetValue_FindBestMatchType () - { -#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - using (var t = new JniType (TestType.JniTypeName)) { - var c = t.GetConstructor ("()V"); - var o = t.NewObject (c, null); - using (var w = valueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) { - Assert.AreEqual (typeof (TestType), w!.GetType ()); - Assert.IsTrue (((TestType) w).ExecutedActivationConstructor); - } - } -#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT - } - - [Test] - public void PeekPeer () - { - Assert.IsNull (valueManager.PeekPeer (new JniObjectReference ())); - - using (var v = new MyDisposableObject ()) { - Assert.IsNotNull (valueManager.PeekPeer (v.PeerReference)); - Assert.AreSame (v, valueManager.PeekPeer (v.PeerReference)); - } - } - - [Test] - public void PeekValue () - { - JniObjectReference lref; - using (var o = new JavaObject ()) { - lref = o.PeerReference.NewLocalRef (); - Assert.AreSame (o, valueManager.PeekValue (lref)); - } - // At this point, the Java-side object is kept alive by `lref`, - // but the wrapper instance has been disposed, and thus should - // be unregistered, and thus unfindable. - Assert.IsNull (valueManager.PeekValue (lref)); - JniObjectReference.Dispose (ref lref); - } - - [Test] - public void PeekValue_BoxedObjects () - { - var marshaler = valueManager.GetValueMarshaler (); - var ad = AppDomain.CurrentDomain; - - var proxy = marshaler.CreateGenericArgumentState (ad); - Assert.AreSame (ad, valueManager.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ad, ref proxy); - - var ex = new InvalidOperationException ("boo!"); - proxy = marshaler.CreateGenericArgumentState (ex); - Assert.AreSame (ex, valueManager.PeekValue (proxy.ReferenceValue)); - marshaler.DestroyGenericArgumentState (ex, ref proxy); - } - - void AllNestedRegistrationScopeTests () - { - AddPeer (); - AddPeer_NoDuplicates (); - ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers (); - CreateValue (); - GetValue_FindBestMatchType (); - GetValue_ReturnsAlias (); - GetValue_ReturnsNullWithInvalidSafeHandle (); - GetValue_ReturnsNullWithNullHandle (); - PeekPeer (); - PeekValue (); - PeekValue_BoxedObjects (); - } - - string DumpPeers () - { - return DumpPeers (valueManager.GetSurfacedPeers ()); - } - - static string DumpPeers (IEnumerable peers) - { - return string.Join ("," + Environment.NewLine, peers); - } - - - // also test: - // Singleton scenario - // Types w/o "activation" constructors -- need to support checking parent scopes - // nesting of scopes - // Adding an instance already added in a previous scope? - } - - public abstract class JniRuntimeJniValueManagerContract< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - T - > : JniRuntimeJniValueManagerContract - { - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] - protected override Type ValueManagerType => typeof (T); - } - -#if !__ANDROID__ - [TestFixture] - public class JniRuntimeJniValueManagerContract_NoGCIntegration : JniRuntimeJniValueManagerContract { - } -#endif // !__ANDROID__ - - // Note: Java side implements JavaInterface, while managed binding DOES NOT. - // This is so that `CreatePeer(…, typeof(IJavaInterface))` tests don't use an existing AnotherJavaInterfaceImpl instance. - // - // This is mostly identical to MyJavaInterfaceImpl; the important difference is that - // it contains an activation constructor, while MyJavaInterfaceImpl does not. - // MyJavaInterfaceImpl can't have one, as that's what provokes the NotSupportedException in the JavaAs() tests. - // - // We want one here so that in "bad" `CreatePeer()` implementations, we'll find this peer and construct it - // before verifying that it satisfies the targetType requirement. - [JniTypeSignature (JniTypeName, GenerateJavaPeer=false)] - public class AnotherJavaInterfaceImpl : JavaObject { - internal const string JniTypeName = "net/dot/jni/test/AnotherJavaInterfaceImpl"; - - internal static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (AnotherJavaInterfaceImpl)); - - internal static Action? BeforeConstruct; - internal static Action? PeerCreated; - - public bool DisposeCalled { - get; - private set; - } - - public override JniPeerMembers JniPeerMembers { - get {return _members;} - } - - unsafe AnotherJavaInterfaceImpl (ref JniObjectReference reference, JniObjectReferenceOptions options) - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - var callback = BeforeConstruct; - BeforeConstruct = null; - callback?.Invoke (reference); - Construct (ref reference, options); - PeerCreated?.Invoke (this); - } - - public unsafe AnotherJavaInterfaceImpl () - : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) - { - const string id = "()V"; - var peer = _members.InstanceMethods.StartCreateInstance (id, GetType (), null); - Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose); - _members.InstanceMethods.FinishCreateInstance (id, this, null); - } - - protected override void Dispose (bool disposing) - { - if (disposing) - DisposeCalled = true; - base.Dispose (disposing); - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs index f28d24727aa..63144cf2fec 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs @@ -1,7 +1,6 @@ using System; using System.Reflection; using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Diagnostics.CodeAnalysis; @@ -11,9 +10,6 @@ namespace Java.InteropTests { -#if !__ANDROID__ - [NonParallelizable] -#endif // !__ANDROID__ [TestFixture] public class JniRuntimeTest : JavaVMFixture { @@ -25,61 +21,12 @@ public void CreateJavaVM () Assert.IsTrue (JniEnvironment.EnvironmentPointer != IntPtr.Zero); } -#if !__ANDROID__ - [Test] - [RequiresDynamicCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not trimming-compatible.")] - public void JDK_OnlySupportsOneVM () - { - try { - var second = new TestJVM (new TestJVMOptions () { - JvmLibraryPath = TestJVM.GetJvmLibraryPath (), - }); - // If we reach here, we're in a JVM that supports > 1 VM - second.Dispose (); - Assert.Ignore (); - } catch (NotSupportedException) { - } catch (Exception e){ - Assert.Fail ("Expected NotSupportedException; got: {0}", e); - } - } - - [Test] - [RequiresDynamicCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not NativeAOT-compatible.")] - [RequiresUnreferencedCode ("This test intentionally uses the default JRE type manager, which is reflection-based and not trimming-compatible.")] - public void UseInvocationPointerOnNewThread () - { - var InvocationPointer = JniRuntime.CurrentRuntime.InvocationPointer; - - var t = new Thread (() => { - try { - var second = new TestJVM (new TestJVMOptions () { - InvocationPointer = InvocationPointer, - }); - } - catch (Exception e) { - Assert.Fail ("Expected no exception, got: {0}", e); - } - }); - t.Start (); - t.Join (); - } -#endif // !__ANDROID__ - [Test] public void CreateJavaVMWithNullBuilder () { Assert.Throws (() => new JavaVMWithNullBuilder ()); } - [Test] - [Category ("TrimmableTypeMapUnsupported")] - public void BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault () - { - var types = JniRuntime.CurrentRuntime.TypeManager.GetTypes (new JniTypeSignature (ManagedPeer.JniTypeName)); - Assert.IsTrue (types.Contains (typeof (ManagedPeer))); - } - class JavaVMWithNullBuilder : JniRuntime { public JavaVMWithNullBuilder () : base ((JniRuntime.CreationOptions) null) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs index 3a407b527d1..ff5fb0d66a1 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs @@ -88,18 +88,6 @@ public void GetTypeSignature_Type () AssertGetJniTypeInfoForType (typeof (JavaArray), "[[I", true, 2); AssertGetJniTypeInfoForType (typeof (JavaArray[]), "[[[I", true, 3); -#if !__ANDROID__ - // Re-enable once typemap files contain `JavaObject` subclasses, not just Java.Lang.Object subclasses - // - // Note: dotnet/android@5c23bcda updates Java.Lang.Object to inherit JavaObject; this is not enough, - // as `` only processes assemblies if they reference Mono.Android.dll. - AssertGetJniTypeInfoForType (typeof (GenericHolder), GenericHolder.JniTypeName, false, 0); - - // XAJavaInterop1 Java Callable Wrappers, as used in dotnet/android, may contain - // `Runtime.register("Java.InteropTests.GenericHolder`1, Mono.Android-Tests", …)`, - // which results in a generic type definition. Permit this. - AssertGetJniTypeInfoForType (typeof (GenericHolder<>), GenericHolder.JniTypeName, false, 0); -#endif // !__ANDROID__ } static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeyword, int arrayRank) diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs index 97397fc70a6..6ef4b07b487 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerAttributeTests.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using Java.Interop; @@ -19,5 +20,55 @@ public void Constructor () Assert.AreEqual (a.MarshalerType, typeof (DemoValueTypeValueMarshaler)); } } -} + [JniValueMarshaler (typeof (DemoValueTypeValueMarshaler))] + struct DemoValueType { + public int Value { get; } + + public DemoValueType (int value) + { + Value = value; + } + } + + class DemoValueTypeValueMarshaler : JniValueMarshaler { + + JniValueMarshaler Int32Marshaler; + + public override bool IsJniValueType => Int32Marshaler.IsJniValueType; + + public DemoValueTypeValueMarshaler () + { + Int32Marshaler = JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); + } + + public override DemoValueType CreateGenericValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType) + { + var value = Int32Marshaler.CreateGenericValue (ref reference, options, typeof (int)); + return new DemoValueType (value); + } + + public override JniValueMarshalerState CreateGenericArgumentState (DemoValueType value, ParameterAttributes synchronize) + { + return Int32Marshaler.CreateGenericArgumentState (value.Value, synchronize); + } + + public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (DemoValueType value, ParameterAttributes synchronize) + { + return Int32Marshaler.CreateGenericObjectReferenceArgumentState (value.Value, synchronize); + } + + public override void DestroyArgumentState (object value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + Int32Marshaler.DestroyArgumentState ((value as DemoValueType?)?.Value, ref state, synchronize); + } + + public override void DestroyGenericArgumentState (DemoValueType value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + Int32Marshaler.DestroyGenericArgumentState (value.Value, ref state, synchronize); + } + } +} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs deleted file mode 100644 index b241836a9d9..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs +++ /dev/null @@ -1,770 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; - -using Java.Interop; -using Java.Interop.Expressions; - -using Mono.Linq.Expressions; - -using NUnit.Framework; - -namespace Java.InteropTests { - - [Category ("TrimmableTypeMapUnsupported")] - public abstract class JniValueMarshalerContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JavaVMFixture - { - - protected abstract T Value {get;} - - protected virtual bool IsJniValueType {get;} - protected virtual bool UsesProxy {get;} - - protected virtual bool Equals (T x, T y) - { - return EqualityComparer.Default.Equals (x, y); - } - - protected virtual void Dispose (T value) - { - } - - protected JniValueMarshaler marshaler; - - protected JniValueMarshalerContractTests (JniValueMarshaler marshaler = null) - { - this.marshaler = marshaler ?? JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); - } - - [Test] - public void TestIsJniValueType () - { - Assert.AreEqual (IsJniValueType, marshaler.IsJniValueType); - } - - [Test] - public void CreateArgumentState () - { - if (IsJniValueType) { - var s = marshaler.CreateArgumentState (Value); - - // Note: Only valid if `Value` isn't 0, so don't do that! - Assert.AreNotEqual (new JniArgumentValue (), s.JniArgumentValue); - - // JniValueMarshaler.CreateArgumentState() for value types only - // fills out the JniArgumentValue value, nothing else. - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.IsNull (s.PeerableValue); - Assert.IsNull (s.Extra); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - else { - var s = marshaler.CreateArgumentState (Value); - - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - } - - [Test] - public void CreateGenericArgumentState () - { - if (IsJniValueType) { - var s = marshaler.CreateGenericArgumentState (Value); - - // Note: Only valid if `Value` isn't 0, so don't do that! - Assert.AreNotEqual (new JniArgumentValue (), s.JniArgumentValue); - - // JniValueMarshaler.CreateArgumentState() for value types only - // fills out the JniArgumentValue value, nothing else. - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.IsNull (s.PeerableValue); - Assert.IsNull (s.Extra); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - else { - var s = marshaler.CreateGenericArgumentState (Value); - - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - } - - [Test] - public void CreateObjectReferenceArgumentState_DefaultValue () - { - var s = marshaler.CreateObjectReferenceArgumentState (default (T)); - - if (IsJniValueType) { - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - } else { - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (), s.JniArgumentValue); - } - Assert.IsNull (s.PeerableValue); - marshaler.DestroyArgumentState (default (T), ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateObjectReferenceArgumentState () - { - var s = marshaler.CreateObjectReferenceArgumentState (Value); - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateGenericObjectReferenceArgumentState_DefaultValue () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (default (T)); - - if (IsJniValueType) { - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - } else { - Assert.IsFalse (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (), s.JniArgumentValue); - } - Assert.IsNull (s.PeerableValue); - marshaler.DestroyGenericArgumentState (default (T), ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateGenericObjectReferenceArgumentState () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (Value); - if (s.PeerableValue != null) { - Assert.AreEqual (s.PeerableValue.PeerReference, s.ReferenceValue); - } - Assert.IsTrue (s.ReferenceValue.IsValid); - Assert.AreEqual (new JniArgumentValue (s.ReferenceValue), s.JniArgumentValue); - - marshaler.DestroyGenericArgumentState (Value, ref s); - Assert.AreEqual (new JniValueMarshalerState (), s); - } - - [Test] - public void CreateValue () - { - var s = marshaler.CreateObjectReferenceArgumentState (Value); - var r = s.ReferenceValue; - var o = marshaler.CreateValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsTrue (Equals (Value, (T) o)); - if (!UsesProxy && !typeof(T).IsValueType) - Assert.AreNotSame (Value, o); - marshaler.DestroyArgumentState (Value, ref s); - - Dispose ((T) o); - } - - [Test] - public void CreateGenericValue () - { - var s = marshaler.CreateGenericObjectReferenceArgumentState (Value); - var r = s.ReferenceValue; - var o = marshaler.CreateGenericValue (ref r, JniObjectReferenceOptions.Copy); - Assert.IsTrue (Equals (Value, o)); - if (!UsesProxy && !typeof(T).IsValueType) - Assert.AreNotSame (Value, o); - marshaler.DestroyGenericArgumentState (Value, ref s); - - Dispose (o); - } - - [Test] - public void DestroyArgumentState () - { - var s = new JniValueMarshalerState (); - marshaler.DestroyArgumentState (null, ref s); - } - - [Test] - public void DestroyGenericArgumentState () - { - var s = new JniValueMarshalerState (); - marshaler.DestroyGenericArgumentState (default (T), ref s); - } - - [Test] - [RequiresUnreferencedCode ("CreateReturnValueFromManagedExpression")] - [RequiresDynamicCode ("CreateReturnValueFromManagedExpression")] - public void CreateReturnValueFromManagedExpression () - { - var runtime = Expression.Variable (typeof (JniRuntime), "__jvm"); - var value = Expression.Variable (typeof (T), "__value"); - var context = new JniValueMarshalerContext (runtime) { - LocalVariables = { - runtime, - value, - }, - }; - var ret = marshaler.CreateReturnValueFromManagedExpression (context, value); - CheckExpression (context, GetExpectedReturnValueFromManagedExpression (runtime.Name, value.Name, ret), ret); - } - - protected virtual string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var valueType = GetTypeName (typeof (T)); - var marshalerType = GetTypeName (marshaler.GetType ()); - return $@"{{ - JniRuntime {jvm}; - {valueType} {value}; - {marshalerType} {value}_marshaler; - JniValueMarshalerState {value}_state; - IntPtr {value}_val; - IntPtr {value}_rtn; - - try - {{ - {value}_marshaler = new {marshalerType}(); - {value}_state = {value}_marshaler.CreateArgumentState((object){value}, ParameterAttributes.None); - {value}_val = {value}_state.ReferenceValue.Handle; - {value}_rtn = References.NewReturnToJniRef({value}_state.ReferenceValue); - return {value}_rtn; - }} - finally - {{ - {value}_marshaler.DestroyArgumentState((object){value}, {value}_state, ParameterAttributes.None); - }} -}}"; - } - - void CheckExpression (JniValueMarshalerContext context, string expected, Expression ret) - { - var body = Expression.Block (context.CreationStatements.Concat (new[]{ ret })); - var cleanup = context.CleanupStatements.Any () - ? (Expression) Expression.Block (context.CleanupStatements.Reverse ()) - : (Expression) Expression.Empty (); - var expr = Expression.TryFinally (body, cleanup); - var block = Expression.Block (context.LocalVariables, expr); - // Convert line endings to Unix style to support building and running on different OS types. - string blockCSharp = block.ToCSharpCode (); - blockCSharp = blockCSharp.Replace ("\r\n", "\n"); - expected = expected.Replace ("\r\n", "\n"); - Console.WriteLine ("# jonp: expected: {0}", GetType ().Name); - Console.WriteLine (blockCSharp); - Assert.AreEqual (expected, blockCSharp); - } - - protected static string GetTypeName (Type type) - { - switch (type.Name) { - case "Boolean": - return "bool"; - case "Byte": - return "byte"; - case "Char": - return "char"; - case "Double": - return "double"; - case "Int16": - return "short"; - case "Int32": - return "int"; - case "Int64": - return "long"; - case "Object": - return "object"; - case "SByte": - return "sbyte"; - case "Single": - return "float"; - } - if (type.IsArray) { - return GetTypeName (type.GetElementType ()) + "[]"; - } - if (!type.IsGenericType) { - return type.Name; - } - var n = new System.Text.StringBuilder (); - n.Append (type.Name); - var b = type.Name.IndexOf ('`'); - n.Remove (b, type.Name.Length - b); - n.Append ("<"); - n.Append (string.Join (", ", type.GenericTypeArguments.Select (tp => GetTypeName (tp)))); - n.Append (">"); - return n.ToString (); - } - } - - [TestFixture] - public class JniValueMarshaler_String_ContractTests : JniValueMarshalerContractTests { - protected override string Value {get {return "value";}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var rname = ((ParameterExpression) ret).Name; - return $@"{{ - JniRuntime {jvm}; - string {value}; - JniObjectReference {value}_ref; - IntPtr {value}_rtn; - - try - {{ - {value}_ref = Strings.NewString({value}); - {value}_rtn = References.NewReturnToJniRef({value}_ref); - return {rname}; - }} - finally - {{ - JniObjectReference.Dispose(__value_ref); - }} -}}"; - } - } - - public abstract class JniValueMarshaler_BuiltinType_ContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JniValueMarshalerContractTests - { - protected override bool IsJniValueType {get {return true;}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var valueType = GetTypeName (typeof (T)); - var rname = ((ParameterExpression) ret).Name; - return $@"{{ - JniRuntime {jvm}; - {valueType} {value}; - - try - {{ - return {rname}; - }} - finally - {{ - default(void); - }} -}}"; - } - } - - [TestFixture] - public class JniValueMarshaler_Boolean_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override bool Value {get {return true;}} - } - - [TestFixture] - public class JniValueMarshaler_SByte_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override sbyte Value {get {return (sbyte) 2;}} - } - - [TestFixture] - public class JniValueMarshaler_Char_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override char Value {get {return '3';}} - } - - [TestFixture] - public class JniValueMarshaler_Int16_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override short Value {get {return (short) 4;}} - } - - [TestFixture] - public class JniValueMarshaler_Int32_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override int Value {get {return 5;}} - } - - [TestFixture] - public class JniValueMarshaler_Int64_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override long Value {get {return 6;}} - } - - [TestFixture] - public class JniValueMarshaler_Single_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override float Value {get {return 7F;}} - } - - [TestFixture] - public class JniValueMarshaler_Double_ContractTests : JniValueMarshaler_BuiltinType_ContractTests { - protected override double Value {get {return 8D;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableBoolean_ContractTests : JniValueMarshalerContractTests { - protected override bool? Value {get {return true;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableSByte_ContractTests : JniValueMarshalerContractTests { - protected override sbyte? Value {get {return (sbyte) 2;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableByte_ContractTests : JniValueMarshalerContractTests { - protected override byte? Value {get {return (byte) 200;}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableChar_ContractTests : JniValueMarshalerContractTests { - protected override char? Value {get {return '3';}} - } - - [TestFixture] - [Category("LLVMIgnore")] //FIXME: https://github.com/dotnet/runtime/issues/89190 - public class JniValueMarshaler_NullableInt16_ContractTests : JniValueMarshalerContractTests { - protected override short? Value {get {return (short) 4;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableInt32_ContractTests : JniValueMarshalerContractTests { - protected override int? Value {get {return 5;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableInt64_ContractTests : JniValueMarshalerContractTests { - protected override long? Value {get {return 6;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableSingle_ContractTests : JniValueMarshalerContractTests { - protected override float? Value {get {return 7F;}} - } - - [TestFixture] - public class JniValueMarshaler_NullableDouble_ContractTests : JniValueMarshalerContractTests { - protected override double? Value {get {return 8D;}} - } - - public abstract class JniInt32ArrayValueMarshalerContractTests< - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JniValueMarshalerContractTests - where T : IEnumerable - { - protected abstract T CreateArray (int[] values); - protected abstract string ValueMarshalerSourceType {get;} - - protected override T Value { - get {return CreateArray (new[]{ 1, 2, 3 });} - } - - protected override bool Equals (T x, T y) - { - return x.SequenceEqual (y); - } - - [Test] - public unsafe void DestroyGenericArgumentState_UpdatesSource () - { - var a = CreateArray (new[]{ 1 }); - var s = marshaler.CreateGenericObjectReferenceArgumentState (a); - fixed (int *p = new[]{3}) - JniEnvironment.Arrays.SetIntArrayRegion (s.ReferenceValue, 0, 1, p); - marshaler.DestroyGenericArgumentState (a, ref s); - Assert.AreEqual (3, a.First ()); - Dispose (a); - } - - protected override void Dispose (T value) - { - var d = value as IDisposable; - if (d != null) { - d.Dispose (); - } - } - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - return $@"{{ - JniRuntime __jvm; - {ValueMarshalerSourceType} __value; - ValueMarshaler __value_marshaler; - JniValueMarshalerState __value_state; - IntPtr __value_val; - IntPtr __value_rtn; - - try - {{ - __value_marshaler = new ValueMarshaler(); - __value_state = __value_marshaler.CreateArgumentState((object)__value, ParameterAttributes.None); - __value_val = __value_state.ReferenceValue.Handle; - __value_rtn = References.NewReturnToJniRef(__value_state.ReferenceValue); - return __value_rtn; - }} - finally - {{ - __value_marshaler.DestroyArgumentState((object)__value, __value_state, ParameterAttributes.None); - }} -}}"; - } - } - - [TestFixture] - public class JniValueMarshaler_Int32Array_ContractTests : JniInt32ArrayValueMarshalerContractTests { - protected override int[] CreateArray (int[] values) {return values;} - - protected override string ValueMarshalerSourceType {get {return "int[]";}} - - [Test] - public unsafe void CreateGenericObjectReferenceArgumentState_OutParameterDoesNotCopy () - { - var a = new[]{ 1 }; - var s = marshaler.CreateGenericObjectReferenceArgumentState (a, ParameterAttributes.Out); - int v; - JniEnvironment.Arrays.GetIntArrayRegion (s.ReferenceValue, 0, 1, &v); - Assert.AreEqual (0, v); - marshaler.DestroyGenericArgumentState (a, ref s); - } - - [Test] - public unsafe void DestroyGenericArgumentState_InParameterDoesNotUpdatesSource () - { - var a = CreateArray (new[]{ 1 }); - var s = marshaler.CreateGenericObjectReferenceArgumentState (a); - fixed (int *p = new[]{3}) - JniEnvironment.Arrays.SetIntArrayRegion (s.ReferenceValue, 0, 1, p); - marshaler.DestroyGenericArgumentState (a, ref s, ParameterAttributes.In); - Assert.AreEqual (1, a.First ()); - Dispose (a); - } - } - - [TestFixture] - public class JniValueMarshaler_ListOfInt32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override IList CreateArray (int[] values) {return values;} - - protected override string ValueMarshalerSourceType {get {return "IList";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaArray_Int32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override JavaArray CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaArray";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaPrimitiveArray_Int32_ContractTests : JniInt32ArrayValueMarshalerContractTests> { - protected override JavaPrimitiveArray CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaPrimitiveArray";}} - } - - [TestFixture] - public class JniValueMarshaler_JavaInt32Array_ContractTests : JniInt32ArrayValueMarshalerContractTests { - protected override JavaInt32Array CreateArray (int[] values) {return new JavaInt32Array (values);} - - protected override string ValueMarshalerSourceType {get {return "JavaInt32Array";}} - } - - [TestFixture] - public class JniValueMarshaler_object_ContractTests : JniValueMarshalerContractTests { - - readonly object value = new object (); - - protected override object Value {get {return value;}} - protected override bool UsesProxy {get {return true;}} - - [Test] - public unsafe void ProxyObjectMethodsDelegateToManagedValue () - { - var value = new ProxyValue (42); - var equalValue = new ProxyValue (42); - var otherValue = new ProxyValue (43); - var valueState = marshaler.CreateGenericObjectReferenceArgumentState (value); - var equalState = marshaler.CreateGenericObjectReferenceArgumentState (equalValue); - var otherState = marshaler.CreateGenericObjectReferenceArgumentState (otherValue); - - try { - using (var proxyType = new JniType ("net/dot/jni/internal/JavaProxyObject")) { - var equals = proxyType.GetInstanceMethod ("equals", "(Ljava/lang/Object;)Z"); - var hashCode = proxyType.GetInstanceMethod ("hashCode", "()I"); - var toString = proxyType.GetInstanceMethod ("toString", "()Ljava/lang/String;"); - var args = stackalloc JniArgumentValue [1]; - - args [0] = new JniArgumentValue (equalState.ReferenceValue); - Assert.IsTrue (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); - - args [0] = new JniArgumentValue (otherState.ReferenceValue); - Assert.IsFalse (JniEnvironment.InstanceMethods.CallBooleanMethod (valueState.ReferenceValue, equals, args)); - - Assert.AreEqual (value.GetHashCode (), JniEnvironment.InstanceMethods.CallIntMethod (valueState.ReferenceValue, hashCode)); - - var proxyString = JniEnvironment.InstanceMethods.CallObjectMethod (valueState.ReferenceValue, toString); - Assert.AreEqual (value.ToString (), JniEnvironment.Strings.ToString (ref proxyString, JniObjectReferenceOptions.CopyAndDispose)); - } - } finally { - marshaler.DestroyGenericArgumentState (otherValue, ref otherState); - marshaler.DestroyGenericArgumentState (equalValue, ref equalState); - marshaler.DestroyGenericArgumentState (value, ref valueState); - } - } - - [Test] - public void SpecificTypesAreUsed () - { - // As a "GREF optimization", the JniValueMarshaler implementation - // will use a "nested" JniValueMarshaler for the runtime type, - // e.g. a boxed int will use JniValueMarshaler - var s = marshaler.CreateGenericObjectReferenceArgumentState (42); - Assert.AreEqual ("java/lang/Integer", JniEnvironment.Types.GetJniTypeNameFromInstance (s.ReferenceValue)); - marshaler.DestroyGenericArgumentState (42, ref s); - - // Compare to the default proxy behavior... - s = marshaler.CreateGenericObjectReferenceArgumentState (value); - Assert.AreEqual ("net/dot/jni/internal/JavaProxyObject", JniEnvironment.Types.GetJniTypeNameFromInstance (s.ReferenceValue)); - marshaler.DestroyGenericArgumentState (value, ref s); - } - - sealed class ProxyValue { - - readonly int value; - - public ProxyValue (int value) - { - this.value = value; - } - - public override bool Equals (object obj) - { - return obj is ProxyValue other && value == other.value; - } - - public override int GetHashCode () - { - return value; - } - - public override string ToString () - { - return $"ProxyValue({value})"; - } - } - } - - [TestFixture] - public class JniValueMarshaler_IJavaPeerable_ContractTests : JniValueMarshalerContractTests { - readonly IJavaPeerable value = new JavaObject (); - - protected override IJavaPeerable Value {get {return value;}} - - protected override string GetExpectedReturnValueFromManagedExpression (string jvm, string value, Expression ret) - { - var pret = (ParameterExpression) ret; - return $@"{{ - JniRuntime {jvm}; - IJavaPeerable {value}; - JniObjectReference {value}_ref; - IntPtr {value}_rtn; - - try - {{ - if (null == {value}) - {{ - return {value}_ref = new JniObjectReference(); - }} - else - {{ - return {value}_ref = (IJavaPeerable){value}.PeerReference; - }} - {value}_rtn = References.NewReturnToJniRef({value}_ref); - return {pret.Name}; - }} - finally - {{ - default(void); - }} -}}"; - } - } - - [JniValueMarshaler (typeof (DemoValueTypeValueMarshaler))] - struct DemoValueType { - public int Value {get;} - - public DemoValueType (int value) - { - Value = value; - } - } - - class DemoValueTypeValueMarshaler : JniValueMarshaler { - - JniValueMarshaler Int32Marshaler; - - public override bool IsJniValueType { - get { - return Int32Marshaler.IsJniValueType; - } - } - - public DemoValueTypeValueMarshaler () - { - Int32Marshaler = JniRuntime.CurrentRuntime.ValueManager.GetValueMarshaler (); - } - - public override DemoValueType CreateGenericValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - Type targetType) - { - var v = Int32Marshaler.CreateGenericValue (ref reference, options, typeof (int)); - return new DemoValueType (v); - } - - public override JniValueMarshalerState CreateGenericArgumentState (DemoValueType value, ParameterAttributes synchronize) - { - return Int32Marshaler.CreateGenericArgumentState (value.Value, synchronize); - } - - public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState (DemoValueType value, ParameterAttributes synchronize) - { - return Int32Marshaler.CreateGenericObjectReferenceArgumentState (value.Value, synchronize); - } - - public override void DestroyArgumentState (object value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - Int32Marshaler.DestroyArgumentState ((value as DemoValueType?)?.Value, ref state, synchronize); - } - - public override void DestroyGenericArgumentState (DemoValueType value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - Int32Marshaler.DestroyGenericArgumentState (value.Value, ref state, synchronize); - } - } - - [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] - class JniValueMarshaler_DemoValueType_ContractTests : JniValueMarshalerContractTests { - - protected override DemoValueType Value {get {return new DemoValueType (42);}} - protected override bool IsJniValueType {get {return true;}} - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs deleted file mode 100644 index 05e8156fa91..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/MethodBindingTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; - -using Java.Interop; - -using NUnit.Framework; - -namespace Java.InteropTests -{ - [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] - public class MethodBindingTests : JavaVMFixture - { - // - // https://bugzilla.xamarin.com/show_bug.cgi?id=17630 - // https://bugzilla.xamarin.com/show_bug.cgi?id=17750#c6 - // https://code.google.com/p/android/issues/detail?id=65710 - // - // The scenario: Assume a class hiearchy three-deep: - // CallNonvirtualBase > CallNonvirtualDerived > CallNonvirtualDerived2 - // - // Historically, "normal" binding rules/convention within Xamarin.Android - // is that if a Java class' Derived method overrides a base method - // without changing anything (visibility, return type), then the Derived - // method is not emitted. - // - // Thus consider the java CallNonvirtualDerived.method() method, which - // overrides but doesn't otherwise change CallNonvirtualBase.method(). - // Consequently, it doesn't need to be emitted, saving on IL size. - // - // Which leads us to the problem: what should the body of the "common" - // CallNonvirtualBase.Method() binding do? It needs to do EITHER a - // virtual method dispatch (invocation on non-public types) OR a - // non-virtual method dispatch (e.g. `base.Method()` invocation). - // - // THEN there are concerns about efficiency and not doing too much extra - // work within the binding methods. - // - // Which brings us to THIS scenario: CallNonvirtualBase declares method(). - // CallNonvirtualDerived overrides method(), but the override isn't - // declared within the CallNonvirtualDerived binding. Finally we have - // CallNonvirtualDerived2, which inherits CallNonvirtualDerived and - // doesn't override anything. - // - // Logically then, if we have a CallNonvirtualDerived2 instance and we - // invoke method() upon it, then we SHOULD invoke - // CallNonvirtualDerived.method(). ASSERT that this is the case. - // Failure to do so possibly results in Bug #17630/#65710, or #17750. - // - // Aside: just to get to this point we're throwing in - // JavaObject.JniThresholdType and JavaObject.JniThresholdClass. - // These likely won't survive long. - // - // Aside: in Bug #17630 terms, CallNonvirtualBase is ContextWrapper, - // CallNonvirtualDerived is Activity, and CallNonvirtualDerived2 is - // an Android Callable Wrapper/user subclass - [Test] - public void VirtualMethodBinding () - { - using (var d = new CallNonvirtualDerived ()) { - d.Method (); - Assert.IsTrue (d.MethodInvoked); - } - using (var b = new CallNonvirtualBase ()) { - b.Method (); - Assert.IsTrue (b.MethodInvoked); - } - using (CallNonvirtualBase b = new CallNonvirtualDerived ()) { - b.Method (); - Assert.IsFalse (b.MethodInvoked); - Assert.IsTrue (((CallNonvirtualDerived) b).MethodInvoked); - } - using (var d2 = new CallNonvirtualDerived2 ()) { - d2.Method (); - Assert.IsTrue (((CallNonvirtualDerived) d2).MethodInvoked); - Assert.IsFalse (((CallNonvirtualBase) d2).MethodInvoked); - } - } - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs index b65e6f08412..3ae85674ca7 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/TestTypeTests.cs @@ -9,7 +9,6 @@ namespace Java.InteropTests { #if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT [TestFixture] - [Category ("TrimmableTypeMapUnsupported")] public class TestTypeTests : JavaVMFixture { int lrefStartCount; diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java deleted file mode 100644 index 02005c388b3..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/AnotherJavaInterfaceImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.dot.jni.test; - -public class AnotherJavaInterfaceImpl - implements JavaInterface, Cloneable -{ - public String getValue() { - return "Another hello from Java!"; - } - - public Object clone() { - return this; - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java deleted file mode 100644 index f4660d0cb87..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorBase.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallVirtualFromConstructorBase implements GCUserPeerable { - - static final String assemblyQualifiedName = "Java.InteropTests.CallVirtualFromConstructorBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public CallVirtualFromConstructorBase (int value) { - if (CallVirtualFromConstructorBase.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "System.Int32", - value - ); - } - calledFromConstructor (value); - } - - public void calledFromConstructor (int value) { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java deleted file mode 100644 index 5ae143daf1c..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CallVirtualFromConstructorDerived.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallVirtualFromConstructorDerived - extends CallVirtualFromConstructorBase - implements GCUserPeerable -{ - static { - net.dot.jni.ManagedPeer.registerNativeMembers ( - CallVirtualFromConstructorDerived.class, - ""); - } - - ArrayList managedReferences = new ArrayList(); - - public CallVirtualFromConstructorDerived (int value) { - super (value); - if (CallVirtualFromConstructorDerived.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - "(I)V", - value - ); - } - } - - public static CallVirtualFromConstructorDerived newInstance (int value) - { - return new CallVirtualFromConstructorDerived (value); - } - - public native void calledFromConstructor (int value); - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} - diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java deleted file mode 100644 index a2c915bfe77..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/CrossReferenceBridge.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CrossReferenceBridge implements GCUserPeerable { - - static final String assemblyQualifiedName = "Java.InteropTests.CrossReferenceBridge, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public CrossReferenceBridge () { - if (CrossReferenceBridge.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java deleted file mode 100644 index d843e43a7c6..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase1.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase1 - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase1 () { - System.out.println("RenameClassBase.()"); - if (RenameClassBase1.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public int hashCode () { - System.out.println("RenameClassBase1.hashCode()"); - return 16; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java deleted file mode 100644 index 562e15bff04..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassBase2.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase2 - extends RenameClassBase1 - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassBase, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase2 () { - System.out.println("RenameClassBase.()"); - if (RenameClassBase2.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - public int hashCode () { - System.out.println("RenameClassBase2.hashCode()"); - return 32; - } - - public int myNewHashCode() { - System.out.println("RenameClassBase2.myNewHashCode()"); - return 33; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java b/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java deleted file mode 100644 index cfbe173f005..00000000000 --- a/external/Java.Interop/tests/Java.Interop-Tests/java/net/dot/jni/test/RenameClassDerived.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassDerived - extends RenameClassBase2 // Note: does NOT match C# binding! This is "post Bytecode rewriting" - implements GCUserPeerable -{ - static final String assemblyQualifiedName = "Java.InteropTests.RenameClassDerived, Java.Interop-Tests"; - - ArrayList managedReferences = new ArrayList(); - - public RenameClassDerived () { - System.out.println("RenameClassDerived.()"); - if (RenameClassDerived.class == getClass ()) { - net.dot.jni.ManagedPeer.construct ( - this, - assemblyQualifiedName, - "" - ); - } - } - - // Note: while *at runtime* `RenameClassBase1` is replaced with `RenameClassBase2`, - // Java Callable Wrapper generator doesn't know about that (yet?), and thus the - // *original* method name will be present. - // Not sure if this is actually a problem; perhaps Bytecode rewriting happens *after* - // Java Callable Wrapper generation? - public int hashCode () { - System.out.println("RenameClassDerived.hashCode()"); - return 64; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj index 280413dd93b..0f67a75f1a9 100644 --- a/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj +++ b/tests/CodeGen-Binding/Xamarin.Android.JcwGen-Tests/Xamarin.Android.JcwGen-Tests.csproj @@ -37,10 +37,17 @@ - - + + + + + + + diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj index 58dc9222be1..38c319b894c 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj @@ -30,24 +30,6 @@ - - - - - - - - - - - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 0d2ffad0efe..7ce9b9366d5 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -9,7 +9,6 @@ Exe true $(XamarinAndroidSourcePath)product.snk - <_SkipJniAddNativeMethodRegistrationAttributeScan>True true $(XamarinAndroidSourcePath)bin\Test$(Configuration) false @@ -89,17 +88,24 @@ - - - - <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> + + + + + + + + + + + - - - - - - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 7ce9b9366d5..d8127037c51 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -88,24 +88,17 @@ + + + + <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> - - - - - - - - - -