From 0ccd3613392456e5cf57e13801ab5578f0e7dd97 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Mon, 24 Nov 2025 14:28:12 +0100 Subject: [PATCH 1/2] [marshal methods] Make AndroidEnvironmentInternal.UnhandledException public Fixes: https://github.com/dotnet/android/issues/10602 Context: https://github.com/dotnet/android/commit/8bc7a3e84f95e70fe12790ac31ecd97957771cb2 This is in the category of "how the hell has it worked before?", as every time the `AndroidEnvironmentInternal.UnhandledException` method living in `Mono.Android.Runtime.dll` would be called, the runtime would respond with a method not accessible exception, rightly so as both the type and the method were internal and visible only to `Mono.Android.dll`. I guess we haven't had many unhandled exceptions in marshal method wrappers over the years? This PR fixes the problem by making both the type and the method public. This is the simplest way to fix the issue. --- .../Android.Runtime/AndroidEnvironmentInternal.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs b/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs index 198e6c92423..c2ba9fafb25 100644 --- a/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs +++ b/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs @@ -2,11 +2,11 @@ namespace Android.Runtime { - internal static class AndroidEnvironmentInternal + public static class AndroidEnvironmentInternal { internal static Action? UnhandledExceptionHandler; - internal static void UnhandledException (Exception e) + public static void UnhandledException (Exception e) { if (UnhandledExceptionHandler == null) { return; From 826cf5b2525940a486768a87e56ba19d5379a886 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Tue, 25 Nov 2025 15:52:37 +0100 Subject: [PATCH 2/2] Rewrite Mono.Android.Runtime.dll if marshal methods are enabled The rewrite changes visibility of the `Android.Runtime.AndroidEnvironmentInternal` type from `internal` to `public` after all the linking and marshal method processing is done. --- .../AndroidEnvironmentInternal.cs | 2 +- .../Tasks/FixUpMonoAndroidRuntime.cs | 37 +++++++ .../MarshalMethodsAssemblyRewriter.cs | 34 +----- .../Utilities/MonoAndroidHelper.cs | 35 ++++++ .../MonoAndroidRuntimeMarshalMethodsFixUp.cs | 102 ++++++++++++++++++ .../Xamarin.Android.Common.targets | 6 ++ 6 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/FixUpMonoAndroidRuntime.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidRuntimeMarshalMethodsFixUp.cs diff --git a/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs b/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs index c2ba9fafb25..d64dadba305 100644 --- a/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs +++ b/src/Mono.Android.Runtime/Android.Runtime/AndroidEnvironmentInternal.cs @@ -2,7 +2,7 @@ namespace Android.Runtime { - public static class AndroidEnvironmentInternal + internal static class AndroidEnvironmentInternal { internal static Action? UnhandledExceptionHandler; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/FixUpMonoAndroidRuntime.cs b/src/Xamarin.Android.Build.Tasks/Tasks/FixUpMonoAndroidRuntime.cs new file mode 100644 index 00000000000..5a8c3a413d7 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/FixUpMonoAndroidRuntime.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; + +namespace Xamarin.Android.Tasks; + +public class FixUpMonoAndroidRuntime : AndroidTask +{ + public override string TaskPrefix => "FUMAR"; + + [Required] + public string IntermediateOutputDirectory { get; set; } = String.Empty; + + [Required] + public ITaskItem[] ResolvedAssemblies { get; set; } = []; + + public override bool RunTask () + { + List monoAndroidRuntimeItems = new (); + foreach (ITaskItem item in ResolvedAssemblies) { + + if (!MonoAndroidHelper.StringEquals (Path.GetFileName (item.ItemSpec), "Mono.Android.Runtime.dll", StringComparison.OrdinalIgnoreCase)) { + continue; + } + monoAndroidRuntimeItems.Add (item); + } + + if (monoAndroidRuntimeItems.Count == 0) { + Log.LogDebugMessage ("No 'Mono.Android.Runtime.dll' items found"); + return !Log.HasLoggedErrors; + } + + return MonoAndroidRuntimeMarshalMethodsFixUp.Run (Log, monoAndroidRuntimeItems) && !Log.HasLoggedErrors; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs index 807632241ba..d63bcc0b104 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -179,39 +179,13 @@ public void Rewrite (bool brokenExceptionTransitions) void CopyFile (string source, string target) { log.LogDebugMessage ($"[{targetArch}] Copying rewritten assembly: {source} -> {target}"); - - string targetBackup = $"{target}.bak"; - if (File.Exists (target)) { - // Try to avoid sharing violations by first renaming the target - File.Move (target, targetBackup); - } - - File.Copy (source, target, true); - - if (File.Exists (targetBackup)) { - try { - File.Delete (targetBackup); - } catch (Exception ex) { - // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. - log.LogDebugMessage ($"[{targetArch}] While trying to delete '{targetBackup}', exception was thrown: {ex}"); - log.LogDebugMessage ($"[{targetArch}] Failed to delete backup file '{targetBackup}', ignoring."); - } - } + MonoAndroidHelper.CopyFileAvoidSharingViolations (log, source, target); } void RemoveFile (string? path) { - if (String.IsNullOrEmpty (path) || !File.Exists (path)) { - return; - } - - try { - log.LogDebugMessage ($"[{targetArch}] Deleting: {path}"); - File.Delete (path); - } catch (Exception ex) { - log.LogWarning ($"[{targetArch}] Unable to delete source file '{path}'"); - log.LogDebugMessage ($"[{targetArch}] {ex.ToString ()}"); - } + log.LogDebugMessage ($"[{targetArch}] Deleting: {path}"); + MonoAndroidHelper.TryRemoveFile (log, path); } static bool HasUnmanagedCallersOnlyAttribute (MethodDefinition method) @@ -504,7 +478,7 @@ MethodDefinition GetUnmanagedCallersOnlyAttributeConstructor (IAssemblyResolver AssemblyDefinition? asm = resolver.Resolve ("System.Runtime.InteropServices"); if (asm == null) throw new ArgumentNullException (nameof (asm)); - + TypeDefinition? unmanagedCallersOnlyAttribute = null; foreach (ModuleDefinition md in asm.Modules) { foreach (ExportedType et in md.ExportedTypes) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index 13102b06955..bf339554775 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -874,5 +874,40 @@ public static void LogTextStreamContents (TaskLoggingHelper log, string message, log.LogDebugMessage (message); log.LogDebugMessage (reader.ReadToEnd ()); } + + public static void CopyFileAvoidSharingViolations (TaskLoggingHelper log, string source, string dest) + { + string destBackup = $"{dest}.bak"; + if (File.Exists (dest)) { + // Try to avoid sharing violations by first renaming the target + File.Move (dest, destBackup); + } + + File.Copy (source, dest, true); + + if (File.Exists (destBackup)) { + try { + File.Delete (destBackup); + } catch (Exception ex) { + // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. + log.LogDebugMessage ($"While trying to delete '{destBackup}', exception was thrown: {ex}"); + log.LogDebugMessage ($"Failed to delete backup file '{destBackup}', ignoring."); + } + } + } + + public static void TryRemoveFile (TaskLoggingHelper log, string? filePath) + { + if (String.IsNullOrEmpty (filePath) || !File.Exists (filePath)) { + return; + } + + try { + File.Delete (filePath); + } catch (Exception ex) { + log.LogWarning ($"Unable to delete source file '{filePath}'"); + log.LogDebugMessage ($"{ex.ToString ()}"); + } + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidRuntimeMarshalMethodsFixUp.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidRuntimeMarshalMethodsFixUp.cs new file mode 100644 index 00000000000..4bdff04800c --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidRuntimeMarshalMethodsFixUp.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Mono.Cecil; + +namespace Xamarin.Android.Tasks; + +class MonoAndroidRuntimeMarshalMethodsFixUp +{ + const string RuntimeTypeName = "Android.Runtime.AndroidEnvironmentInternal"; + + public static bool Run (TaskLoggingHelper log, List items) + { + bool everythingWorked = true; + foreach (ITaskItem item in items) { + if (!ApplyFixUp (log, item)) { + everythingWorked = false; + } + } + + return everythingWorked; + } + + static bool ApplyFixUp (TaskLoggingHelper log, ITaskItem monoAndroidRuntime) + { + string newDirPath = Path.Combine (Path.GetDirectoryName (monoAndroidRuntime.ItemSpec), "new"); + string newFilePath = Path.Combine (newDirPath, Path.GetFileName (monoAndroidRuntime.ItemSpec)); + Directory.CreateDirectory (newDirPath); + + string origPdbPath = Path.ChangeExtension (monoAndroidRuntime.ItemSpec, ".pdb"); + bool havePdb = File.Exists (origPdbPath); + + log.LogDebugMessage ($"Fixing up {monoAndroidRuntime.ItemSpec}"); + var readerParams = new ReaderParameters () { + InMemory = true, + ReadSymbols = havePdb, + }; + AssemblyDefinition asmdef = AssemblyDefinition.ReadAssembly (monoAndroidRuntime.ItemSpec, readerParams); + TypeDefinition? androidRuntimeInternal = null; + foreach (ModuleDefinition module in asmdef.Modules) { + androidRuntimeInternal = FindAndroidRuntimeInternal (module); + if (androidRuntimeInternal != null) { + break; + } + } + + if (androidRuntimeInternal == null) { + log.LogDebugMessage ($"'{RuntimeTypeName}' not found in {monoAndroidRuntime.ItemSpec}"); + return true; // Not an error, per se... + } + log.LogDebugMessage ($"Found '{RuntimeTypeName}', making it public"); + androidRuntimeInternal.IsPublic = true; + + var writerParams = new WriterParameters { + WriteSymbols = havePdb, + }; + asmdef.Write (newFilePath, writerParams); + + CopyFile (log, newFilePath, monoAndroidRuntime.ItemSpec); + RemoveFile (log, newFilePath); + + if (!havePdb) { + return true; + } + + string pdbPath = Path.ChangeExtension (newFilePath, ".pdb"); + havePdb = File.Exists (pdbPath); + if (!havePdb) { + return true; + } + + CopyFile (log, pdbPath, origPdbPath); + RemoveFile (log, pdbPath); + + return true; + } + + static void CopyFile (TaskLoggingHelper log, string source, string target) + { + log.LogDebugMessage ($"Copying rewritten assembly: {source} -> {target}"); + MonoAndroidHelper.CopyFileAvoidSharingViolations (log, source, target); + } + + static void RemoveFile (TaskLoggingHelper log, string? path) + { + log.LogDebugMessage ($"Deleting: {path}"); + MonoAndroidHelper.TryRemoveFile (log, path); + } + + static TypeDefinition? FindAndroidRuntimeInternal (ModuleDefinition module) + { + foreach (TypeDefinition t in module.Types) { + if (MonoAndroidHelper.StringEquals (RuntimeTypeName, t.FullName)) { + return t; + } + } + + return null; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index ee24d3a843d..a33f0dfe319 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -103,6 +103,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. +