From adce73b0b7411abe8072b0375a6ff658b68fc503 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 29 Jul 2026 08:04:25 -0500 Subject: [PATCH] [CoreCLR] Stabilize Debug typemaps across C# rebuilds (#12216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoreCLR Debug typemaps embedded application assembly MVIDs. Because an MVID changes on every compilation, a C#-only rebuild rewrote the native typemap even when no Java mappings had changed. That cascaded into native compilation, APK rebuilding, and re-signing on every Fast Deployment cycle. This changes Debug managed-to-Java entries to key on stable assembly full names and removes the Debug MVID table entirely. Release continues to use its MVID-based lookup, and a trimmer feature switch prevents `Assembly.FullName` retrieval and marshalling from being rooted in Release builds. ## Runtime `clr_typemap_managed_to_java` now takes an assembly display name alongside the type name, and `TypeMapper::managed_to_java` is split on `RELEASE` so each configuration only sees the parameter it uses. When `$(RuntimeFeature.ManagedToJavaUsesAssemblyFullName)` is enabled, `JNIEnv.TypemapManagedToJava` skips computing the MVID altogether rather than computing it and discarding it. A null display name is left for the native side to report, which keeps a single diagnostic path for all callers. `TypeMapCecilAdapter.GetRuntimeAssemblyFullName` reproduces reflection's escaping and `PublicKeyToken` formatting so that the build-time name matches what `Assembly.FullName` returns at runtime; a mismatch here would silently break every lookup. ## Tests `IncrementalBuildTest` performs a C#-only change on the CoreCLR Debug path and asserts the typemap and signed APK are byte-for-byte unchanged, and that `_CompileNativeAssemblySources`, `_CreateApplicationSharedLibraries`, `_BuildApkFastDev`, and `_Sign` are all skipped. `FastDeployUpdatesTypeMapAfterAssemblyEdit` is a device test covering three deployments, parameterized over both `FastDeploy` and `FastDeploy2`: 1. Deploy with only `MainActivity`. 2. Add a `SecondActivity`. This is a C#-only edit, but it introduces a new Java-callable type. Fast deployment only syncs managed assemblies, so the Java stubs, `.dex`, typemap, APK and signature must all be rebuilt and the package reinstalled. This confirms a typemap-affecting C# change is picked up automatically, with no clean required. 3. Add a `Console.WriteLine()` to `SecondActivity.OnCreate()`. No Java-callable types change, so the packaging and signing targets are skipped and the APK is not reinstalled. The message is asserted in `logcat` to prove the updated assembly really was deployed rather than merely that nothing was rebuilt. `BuildOutput.IsApkInstalled` now throws below `LoggerVerbosity.Detailed`. It scans for an `Installed Package` message logged at `MessageImportance.Low`, so at a lower verbosity it silently returned `false` no matter what the build did — which broke the new test on CI and had been quietly making some pre-existing `Assert.IsFalse` calls vacuous. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 9cc92b0805a941f813fc76ae4b7b00849a66368a) --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 24 +++--- .../Android.Runtime/RuntimeNativeMethods.cs | 2 +- .../RuntimeFeature.cs | 6 ++ .../MonoDroid.Tuner/FindTypeMapObjectsStep.cs | 1 + .../Microsoft.Android.Sdk.CoreCLR.targets | 4 + .../Tasks/GenerateEmptyTypemapStub.cs | 6 +- .../IncrementalBuildTest.cs | 26 ++++++ .../Tasks/LlvmIrGeneratorTests.cs | 24 ++++++ .../Common/BuildOutput.cs | 20 +++++ .../Utilities/TypeMapCecilAdapter.cs | 19 ++++ .../Utilities/TypeMapGenerator.cs | 1 + .../Utilities/TypeMapObjectsXmlFile.cs | 16 ++-- ...eMappingDebugNativeAssemblyGeneratorCLR.cs | 86 +++---------------- src/native/clr/host/internal-pinvokes-clr.cc | 10 ++- src/native/clr/host/typemap.cc | 35 +++----- src/native/clr/include/host/typemap.hh | 6 +- .../include/runtime-base/internal-pinvokes.hh | 2 +- src/native/clr/include/xamarin-app.hh | 10 --- .../xamarin-app-stub/application_dso_stub.cc | 2 - .../nativeaot/host/internal-pinvoke-stubs.cc | 1 + .../Tests/FastDevTest.cs | 83 +++++++++++++++--- 21 files changed, 244 insertions(+), 140 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 234913e3d63..2b014fe3a49 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -441,16 +441,19 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } - if (mvid_bytes == null) - mvid_bytes = new byte[16]; - - var mvid = new Span(mvid_bytes); byte[]? mvid_data = null; - 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; + // The Debug CoreCLR typemaps are keyed on the assembly display name, so computing the MVID would be wasted work. + if (!RuntimeFeature.IsCoreClrRuntime || !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; + } } IntPtr ret; @@ -460,7 +463,8 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) } else if (RuntimeFeature.IsCoreClrRuntime) { if (type.FullName is null) return null; - ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, (IntPtr)mvidptr); + string? assemblyFullName = RuntimeFeature.ManagedToJavaUsesAssemblyFullName ? type.Assembly.FullName : null; + ret = RuntimeNativeMethods.clr_typemap_managed_to_java (type.FullName, assemblyFullName, (IntPtr)mvidptr); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } diff --git a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs index 551b76516fd..a2a59d33ba4 100644 --- a/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs +++ b/src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs @@ -115,7 +115,7 @@ internal unsafe static partial class RuntimeNativeMethods [LibraryImport (RuntimeConstants.InternalDllName, StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] - internal static partial IntPtr clr_typemap_managed_to_java (string fullName, IntPtr mvid); + 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) })] diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index f15a79ff6f4..718f0b6f656 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -12,6 +12,7 @@ static class RuntimeFeature const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; + const bool ManagedToJavaUsesAssemblyFullNameEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -44,4 +45,9 @@ static class RuntimeFeature [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}")] internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; + + // 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/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs index 08863825ef9..43a9b8de334 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindTypeMapObjectsStep.cs @@ -37,6 +37,7 @@ public void ProcessAssembly (AssemblyDefinition assembly, StepContext context) var xml = new TypeMapObjectsXmlFile { AssemblyName = assembly.Name.Name, + AssemblyFullName = Debug ? TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name) : null, AssemblyMvid = assembly.MainModule.Mvid, }; 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 27a57454aeb..8368d353fc0 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 @@ -34,6 +34,10 @@ This file contains the CoreCLR-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + + /// Gets a value indicating whether the .apk was installed on the device by the last build. + /// + /// + /// This scans the build output for the Installed Package message written by the + /// FastDeploy and FastDeploy2 tasks. Both write it with + /// , so it is only present in the + /// build output at or higher. + /// At a lower verbosity this property would silently return false no matter what the + /// build did, which makes both Assert.IsTrue and Assert.IsFalse on it meaningless. + /// It therefore throws instead, so the test fails with an actionable message rather than a + /// misleading pass or an opaque assertion failure. + /// + /// + /// is running at a verbosity below + /// . + /// public bool IsApkInstalled { get { + if (Builder.Verbosity < LoggerVerbosity.Detailed) + throw new InvalidOperationException ($"`{nameof (IsApkInstalled)}` requires `{nameof (Builder)}.{nameof (Builder.Verbosity)}` to be `{nameof (LoggerVerbosity.Detailed)}` or higher, but it is `{Builder.Verbosity}`. The `Installed Package` message it looks for is logged with `MessageImportance.Low`."); foreach (var line in Builder.LastBuildOutput) { if (line.Contains ("Installed Package") || line.Contains (" pm install ")) return true; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs index 94b1018ae4f..f3327506f2c 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapCecilAdapter.cs @@ -3,6 +3,9 @@ using Java.Interop.Tools.Cecil; using Mono.Cecil; +using ReflectionAssemblyContentType = System.Reflection.AssemblyContentType; +using ReflectionAssemblyName = System.Reflection.AssemblyName; +using ReflectionAssemblyNameFlags = System.Reflection.AssemblyNameFlags; using ModuleReleaseData = Xamarin.Android.Tasks.TypeMapGenerator.ModuleReleaseData; using ReleaseGenerationState = Xamarin.Android.Tasks.TypeMapGenerator.ReleaseGenerationState; using TypeMapDebugEntry = Xamarin.Android.Tasks.TypeMapGenerator.TypeMapDebugEntry; @@ -149,9 +152,25 @@ static TypeMapDebugEntry GetDebugEntry (TypeDefinition td, TypeDefinitionCache c TypeDefinition = td, SkipInJavaToManaged = ShouldSkipInJavaToManaged (td), AssemblyName = td.Module.Assembly.Name.Name, + AssemblyFullName = GetRuntimeAssemblyFullName (td.Module.Assembly.Name), }; } + // Cecil's FullName does not escape assembly display names in the same way as Assembly.FullName. + public static string GetRuntimeAssemblyFullName (AssemblyNameReference assemblyName) + { + var runtimeAssemblyName = new ReflectionAssemblyName { + Name = assemblyName.Name, + Version = assemblyName.Version, + CultureName = assemblyName.Culture ?? "", + Flags = assemblyName.IsRetargetable ? ReflectionAssemblyNameFlags.Retargetable : ReflectionAssemblyNameFlags.None, + ContentType = assemblyName.IsWindowsRuntime ? ReflectionAssemblyContentType.WindowsRuntime : ReflectionAssemblyContentType.Default, + }; + runtimeAssemblyName.SetPublicKeyToken (assemblyName.PublicKeyToken); + + return runtimeAssemblyName.FullName ?? throw new InvalidOperationException ($"Unable to format assembly name '{assemblyName.Name}'."); + } + static string GetManagedTypeName (TypeDefinition td) { // This is necessary because Mono runtime will return to us type name with a `.` for nested types (not a diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs index 41e0d4d2490..6fd04538052 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapGenerator.cs @@ -64,6 +64,7 @@ internal sealed class TypeMapDebugEntry public bool SkipInJavaToManaged; public TypeMapDebugEntry DuplicateForJavaToManaged; public string AssemblyName; + public string AssemblyFullName; // This field is only used by the Cecil adapter for temp storage while reading. // It is not used to create the typemap. diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs index 1a15f000f0c..0d38817c825 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMapObjectsXmlFile.cs @@ -23,6 +23,7 @@ class TypeMapObjectsXmlFile static readonly TypeMapObjectsXmlFile unscanned = new TypeMapObjectsXmlFile { WasScanned = false }; public string? AssemblyName { get; set; } + public string? AssemblyFullName { get; set; } public Guid AssemblyMvid { get; set; } = Guid.Empty; public bool FoundJniNativeRegistration { get; set; } public List JavaToManagedDebugEntries { get; } = []; @@ -57,6 +58,7 @@ void Export (XmlWriter xml) xml.WriteStartElement ("api"); xml.WriteAttributeString ("type", HasDebugEntries ? "debug" : "release"); xml.WriteAttributeStringIfNotDefault ("assembly-name", AssemblyName); + xml.WriteAttributeStringIfNotDefault ("assembly-full-name", AssemblyFullName); if (AssemblyMvid != Guid.Empty) { xml.WriteAttributeString ("mvid", AssemblyMvid.ToString ("N")); @@ -183,6 +185,7 @@ public static TypeMapObjectsXmlFile Import (string filename) throw new InvalidOperationException ($"Missing required attribute 'type' in '{filename}'"); var assemblyName = reader.GetAttribute ("assembly-name"); + var assemblyFullName = reader.GetAttribute ("assembly-full-name"); var mvidValue = reader.GetAttribute ("mvid"); var mvid = mvidValue.IsNullOrWhiteSpace () ? Guid.Empty : Guid.Parse (mvidValue); var foundJniNativeRegistration = GetAttributeOrDefault (reader, "found-jni-native-registration", false); @@ -190,6 +193,7 @@ public static TypeMapObjectsXmlFile Import (string filename) var file = new TypeMapObjectsXmlFile { WasScanned = true, AssemblyName = assemblyName, + AssemblyFullName = assemblyFullName, AssemblyMvid = mvid, FoundJniNativeRegistration = foundJniNativeRegistration, }; @@ -205,6 +209,7 @@ public static TypeMapObjectsXmlFile Import (string filename) static void ImportDebugData (XmlReader reader, TypeMapObjectsXmlFile file) { var assemblyName = file.AssemblyName ?? string.Empty; + var assemblyFullName = file.AssemblyFullName ?? string.Empty; var isMonoAndroid = assemblyName == "Mono.Android"; while (reader.Read ()) { @@ -212,9 +217,9 @@ static void ImportDebugData (XmlReader reader, TypeMapObjectsXmlFile file) continue; if (reader.Name == "java-to-managed") - ReadDebugEntries (reader, file.JavaToManagedDebugEntries, assemblyName, isMonoAndroid); + ReadDebugEntries (reader, file.JavaToManagedDebugEntries, assemblyName, assemblyFullName, isMonoAndroid); else if (reader.Name == "managed-to-java") - ReadDebugEntries (reader, file.ManagedToJavaDebugEntries, assemblyName, isMonoAndroid); + ReadDebugEntries (reader, file.ManagedToJavaDebugEntries, assemblyName, assemblyFullName, isMonoAndroid); } } @@ -268,7 +273,7 @@ public static void WriteEmptyFile (string destination, TaskLoggingHelper log) File.Create (destination).Dispose (); } - static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyName, bool isMonoAndroid) + static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyName, string assemblyFullName, bool isMonoAndroid) { return new TypeMapDebugEntry { JavaName = reader.GetAttribute ("java-name") ?? string.Empty, @@ -278,6 +283,7 @@ static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyNam IsInvoker = GetAttributeOrDefault (reader, "is-invoker", false), IsMonoAndroid = isMonoAndroid, AssemblyName = assemblyName, + AssemblyFullName = assemblyFullName, }; } @@ -301,7 +307,7 @@ static T GetAttributeOrDefault (XmlReader reader, string name, T defaultValue return (T) Convert.ChangeType (value, typeof (T), CultureInfo.InvariantCulture); } - static void ReadDebugEntries (XmlReader reader, List entries, string assemblyName, bool isMonoAndroid) + static void ReadDebugEntries (XmlReader reader, List entries, string assemblyName, string assemblyFullName, bool isMonoAndroid) { if (reader.IsEmptyElement) return; @@ -313,7 +319,7 @@ static void ReadDebugEntries (XmlReader reader, List entries, return; if (reader.NodeType == XmlNodeType.Element && reader.Name == "entry") - entries.Add (FromDebugEntryXml (reader, assemblyName, isMonoAndroid)); + entries.Add (FromDebugEntryXml (reader, assemblyName, assemblyFullName, isMonoAndroid)); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs index db3640f204c..62e27292ecf 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs @@ -14,7 +14,6 @@ class TypeMappingDebugNativeAssemblyGeneratorCLR : LlvmIrComposer // These names MUST match src/native/clr/include/xamarin-app.hh const string TypeMapSymbol = "type_map"; - const string UniqueAssembliesSymbol = "type_map_unique_assemblies"; const string AssemblyNamesBlobSymbol = "type_map_assembly_names"; const string ManagedTypeNamesBlobSymbol = "type_map_managed_type_names"; const string JavaTypeNamesBlobSymbol = "type_map_java_type_names"; @@ -66,24 +65,6 @@ public override string GetComment (object data, string fieldName) } } - sealed class TypeMapAssemblyContextDataProvider : NativeAssemblerStructContextDataProvider - { - public override string GetComment (object data, string fieldName) - { - var entry = EnsureType (data); - - if (MonoAndroidHelper.StringEquals ("module_uuid", fieldName)) { - return $" MVID: {entry.MVID}"; - } - - if (MonoAndroidHelper.StringEquals ("name_offset", fieldName)) { - return $" {entry.Name}"; - } - - return String.Empty; - } - } - sealed class TypeMapManagedTypeInfoContextDataProvider : NativeAssemblerStructContextDataProvider { public override string GetComment (object data, string fieldName) @@ -153,7 +134,6 @@ sealed class TypeMap public int ManagedToJavaCount; public uint entry_count; - public ulong unique_assemblies_count; [NativeAssembler (UsesDataProvider = true), NativePointer (PointsToSymbol = "")] public TypeMapEntry? java_to_managed = null; @@ -162,34 +142,12 @@ sealed class TypeMap public TypeMapEntry? managed_to_java = null; }; - // Order of fields and their type must correspond *exactly* to that in - // src/native/clr/include/xamarin-app.hh TypeMapAssembly structure - [NativeAssemblerStructContextDataProvider (typeof (TypeMapAssemblyContextDataProvider))] - sealed class TypeMapAssembly - { - [NativeAssembler (Ignore = true)] - public string Name = String.Empty; - - [NativeAssembler (Ignore = true)] - public Guid MVID; - - [NativeAssembler (UsesDataProvider = true, InlineArray = true, InlineArraySize = 16)] - public byte[] module_uuid = []; - - public ulong name_length; - - [NativeAssembler (UsesDataProvider = true)] - public ulong name_offset; - } - readonly TypeMapGenerator.ModuleDebugData data; StructureInfo? typeMapEntryStructureInfo; StructureInfo? typeMapStructureInfo; - StructureInfo? typeMapAssemblyStructureInfo; StructureInfo? typeMapManagedTypeInfoStructureInfo; List> javaToManagedMap; List> managedToJavaMap; - List> uniqueAssemblies; List> managedTypeInfos; StructureInstance? type_map; @@ -204,7 +162,6 @@ public TypeMappingDebugNativeAssemblyGeneratorCLR (TaskLoggingHelper log, TypeMa javaToManagedMap = new (); managedToJavaMap = new (); - uniqueAssemblies = new (); managedTypeInfos = new (); } @@ -225,14 +182,23 @@ protected override void Construct (LlvmIrModule module) // in a callback during code generation foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.ManagedToJavaMap) { - (int managedTypeNameOffset, int _) = managedTypeNames.Add (entry.ManagedName); + if (String.IsNullOrEmpty (entry.AssemblyFullName)) { + throw new InvalidOperationException ($"Internal error: assembly full name is missing for managed type '{entry.ManagedName}'."); + } + + if (!entry.ManagedName.EndsWith (entry.AssemblyName, StringComparison.Ordinal)) { + throw new InvalidOperationException ($"Internal error: managed type name '{entry.ManagedName}' does not end with assembly name '{entry.AssemblyName}'."); + } + + string managedName = entry.ManagedName.Substring (0, entry.ManagedName.Length - entry.AssemblyName.Length) + entry.AssemblyFullName; + (int managedTypeNameOffset, int _) = managedTypeNames.Add (managedName); (int javaTypeNameOffset, int _) = javaTypeNames.Add (entry.JavaName); var m2j = new TypeMapEntry { - From = entry.ManagedName, + From = managedName, To = entry.JavaName, from = (uint)managedTypeNameOffset, - from_hash = TypeMapHelper.HashNameForCLR (entry.ManagedName), + from_hash = TypeMapHelper.HashNameForCLR (managedName), to = (uint)javaTypeNameOffset, }; managedToJavaMap.Add (new StructureInstance (typeMapEntryStructureInfo, m2j)); @@ -252,32 +218,11 @@ protected override void Construct (LlvmIrModule module) }); var assemblyNamesBlob = new LlvmIrStringBlob (); + data.UniqueAssemblies.Sort ((a, b) => String.Compare (a.Name, b.Name, StringComparison.Ordinal)); foreach (TypeMapGenerator.TypeMapDebugAssembly asm in data.UniqueAssemblies) { - (int assemblyNameOffset, int assemblyNameLength) = assemblyNamesBlob.Add (asm.Name); - - var entry = new TypeMapAssembly { - Name = asm.Name, - MVID = asm.MVID, - - module_uuid = asm.MVIDBytes, - name_length = (ulong)assemblyNameLength, // without the trailing NUL - name_offset = (ulong)assemblyNameOffset, - }; - uniqueAssemblies.Add (new StructureInstance (typeMapAssemblyStructureInfo, entry)); + assemblyNamesBlob.Add (asm.Name); } - uniqueAssemblies.Sort ((StructureInstance a, StructureInstance b) => { - if (a.Instance == null) { - return b.Instance == null ? 0 : -1; - } - - if (b.Instance == null) { - return 1; - } - - return a.Instance.module_uuid.AsSpan ().SequenceCompareTo (b.Instance.module_uuid); - }); - var managedTypeInfos = new List> (); // Java-to-managed maps don't use hashes since many mappings have multiple instances foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.JavaToManagedMap) { @@ -315,7 +260,6 @@ protected override void Construct (LlvmIrModule module) ManagedToJavaCount = data.ManagedToJavaMap == null ? 0 : data.ManagedToJavaMap.Count, entry_count = data.EntryCount, - unique_assemblies_count = (ulong)data.UniqueAssemblies.Count, }; type_map = new StructureInstance (typeMapStructureInfo, map); @@ -323,7 +267,6 @@ protected override void Construct (LlvmIrModule module) module.AddGlobalVariable (ManagedToJavaSymbol, managedToJavaMap, LlvmIrVariableOptions.LocalConstant); module.AddGlobalVariable (JavaToManagedSymbol, javaToManagedMap, LlvmIrVariableOptions.LocalConstant); module.AddGlobalVariable (TypeMapManagedTypeInfoSymbol, managedTypeInfos, LlvmIrVariableOptions.GlobalConstant); - module.AddGlobalVariable (UniqueAssembliesSymbol, uniqueAssemblies, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (AssemblyNamesBlobSymbol, assemblyNamesBlob, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (ManagedTypeNamesBlobSymbol, managedTypeNames, LlvmIrVariableOptions.GlobalConstant); module.AddGlobalVariable (JavaTypeNamesBlobSymbol, javaTypeNames, LlvmIrVariableOptions.GlobalConstant); @@ -331,7 +274,6 @@ protected override void Construct (LlvmIrModule module) void MapStructures (LlvmIrModule module) { - typeMapAssemblyStructureInfo = module.MapStructure (); typeMapEntryStructureInfo = module.MapStructure (); typeMapStructureInfo = module.MapStructure (); typeMapManagedTypeInfoStructureInfo = module.MapStructure (); diff --git a/src/native/clr/host/internal-pinvokes-clr.cc b/src/native/clr/host/internal-pinvokes-clr.cc index 59d7e218c30..844f9b748f0 100644 --- a/src/native/clr/host/internal-pinvokes-clr.cc +++ b/src/native/clr/host/internal-pinvokes-clr.cc @@ -9,9 +9,17 @@ using namespace xamarin::android; -const char* clr_typemap_managed_to_java (const char *typeName, const uint8_t *mvid) noexcept +const char* clr_typemap_managed_to_java ( + const char *typeName, + [[maybe_unused]] const char *assemblyFullName, + [[maybe_unused]] const uint8_t *mvid +) noexcept { +#if defined(RELEASE) return TypeMapper::managed_to_java (typeName, mvid); +#else + return TypeMapper::managed_to_java (typeName, assemblyFullName); +#endif } bool clr_typemap_java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept diff --git a/src/native/clr/host/typemap.cc b/src/native/clr/host/typemap.cc index 020ec84669a..71748a13410 100644 --- a/src/native/clr/host/typemap.cc +++ b/src/native/clr/host/typemap.cc @@ -150,26 +150,14 @@ auto TypeMapper::index_to_name (ssize_t idx, const char* typeName, const TypeMap } [[gnu::always_inline, gnu::flatten]] -auto TypeMapper::managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char* +auto TypeMapper::managed_to_java_debug (const char *typeName, const char *assemblyFullName) noexcept -> const char* { dynamic_local_path_string full_type_name; full_type_name.append (typeName); + full_type_name.append (", "sv); + full_type_name.append (assemblyFullName); - auto equal = [](TypeMapAssembly const& entry, const uint8_t *key) -> bool { return memcmp (entry.module_uuid, key, sizeof(entry.module_uuid)) == 0; }; - auto less_than = [](TypeMapAssembly const& entry, const uint8_t *key) -> bool { return memcmp (entry.module_uuid, key, sizeof(entry.module_uuid)) < 0; }; - ssize_t idx = Search::binary_search (mvid, type_map_unique_assemblies, type_map.unique_assemblies_count); - - if (idx >= 0) [[likely]] { - TypeMapAssembly const& assm = type_map_unique_assemblies[idx]; - full_type_name.append (", "sv); - - // We explicitly trust the build process here, with regards to validity of offsets - full_type_name.append (&type_map_assembly_names[assm.name_offset], assm.name_length); - } else { - log_warn (LOG_ASSEMBLY, "typemap: unable to look up assembly name for type '{}', trying without it."sv, typeName); - } - - idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); + ssize_t idx = find_index_by_hash (full_type_name.get (), type_map.managed_to_java, type_map_managed_type_names, MANAGED, JAVA); return index_to_name (idx, full_type_name.get (), type_map.managed_to_java, type_map_java_type_names, MANAGED, JAVA); } @@ -320,7 +308,11 @@ auto TypeMapper::managed_to_java_release (const char *typeName, const uint8_t *m #endif // def RELEASE [[gnu::flatten]] +#if defined(RELEASE) auto TypeMapper::managed_to_java (const char *typeName, const uint8_t *mvid) noexcept -> const char* +#else +auto TypeMapper::managed_to_java (const char *typeName, const char *assemblyFullName) noexcept -> const char* +#endif { log_debug (LOG_ASSEMBLY, "managed_to_java: looking up type '{}'"sv, optional_string (typeName)); if (FastTiming::enabled ()) [[unlikely]] { @@ -332,14 +324,15 @@ auto TypeMapper::managed_to_java (const char *typeName, const uint8_t *mvid) noe return nullptr; } - auto do_map = [&typeName, &mvid]() -> const char* { #if defined(RELEASE) - return managed_to_java_release (typeName, mvid); + const char *ret = managed_to_java_release (typeName, mvid); #else - return managed_to_java_debug (typeName, mvid); + if (assemblyFullName == nullptr) [[unlikely]] { + log_warnf (LOG_ASSEMBLY, "typemap: assembly full name not specified in typemap_managed_to_java"); + return nullptr; + } + const char *ret = managed_to_java_debug (typeName, assemblyFullName); #endif - }; - const char *ret = do_map (); if (FastTiming::enabled ()) [[unlikely]] { internal_timing.end_event (); diff --git a/src/native/clr/include/host/typemap.hh b/src/native/clr/include/host/typemap.hh index 6a3ee0565e5..d8dbe37cc9d 100644 --- a/src/native/clr/include/host/typemap.hh +++ b/src/native/clr/include/host/typemap.hh @@ -13,7 +13,11 @@ namespace xamarin::android { static constexpr std::string_view JAVA { "Java" }; public: +#if defined(RELEASE) static auto managed_to_java (const char *typeName, const uint8_t *mvid) noexcept -> const char*; +#else + static auto managed_to_java (const char *typeName, const char *assemblyFullName) noexcept -> const char*; +#endif static auto java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept -> bool; private: @@ -29,7 +33,7 @@ namespace xamarin::android { static auto index_to_name (ssize_t index, const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) -> const char*; static auto find_index_by_hash (const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) noexcept -> ssize_t; static auto find_index_by_name (const char *typeName, const TypeMapEntry *map, const char (&name_map)[], std::string_view const& from_name, std::string_view const& to_name) noexcept -> ssize_t; - static auto managed_to_java_debug (const char *typeName, const uint8_t *mvid) noexcept -> const char*; + static auto managed_to_java_debug (const char *typeName, const char *assemblyFullName) noexcept -> const char*; static auto java_to_managed_debug (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept -> bool; #endif }; diff --git a/src/native/clr/include/runtime-base/internal-pinvokes.hh b/src/native/clr/include/runtime-base/internal-pinvokes.hh index 5f67a5397f9..a5408b45046 100644 --- a/src/native/clr/include/runtime-base/internal-pinvokes.hh +++ b/src/native/clr/include/runtime-base/internal-pinvokes.hh @@ -15,7 +15,7 @@ extern "C" { void _monodroid_gref_log (const char *message) noexcept; int _monodroid_gref_log_new (jobject curHandle, char curType, jobject newHandle, char newType, const char *threadName, int threadId, const char *from, int from_writable) noexcept; void _monodroid_gref_log_delete (jobject handle, char type, const char *threadName, int threadId, const char *from, int from_writable) noexcept; - const char* clr_typemap_managed_to_java (const char *typeName, const uint8_t *mvid) noexcept; + const char* clr_typemap_managed_to_java (const char *typeName, const char *assemblyFullName, const uint8_t *mvid) noexcept; bool clr_typemap_java_to_managed (const char *java_type_name, char const** assembly_name, uint32_t *managed_type_token_id) noexcept; BridgeProcessingFtn clr_initialize_gc_bridge ( BridgeProcessingStartedFtn bridge_processing_started_callback, diff --git a/src/native/clr/include/xamarin-app.hh b/src/native/clr/include/xamarin-app.hh index e6335f52db6..1fdb13b78a4 100644 --- a/src/native/clr/include/xamarin-app.hh +++ b/src/native/clr/include/xamarin-app.hh @@ -60,18 +60,9 @@ struct TypeMapManagedTypeInfo struct TypeMap { uint32_t entry_count; - uint64_t unique_assemblies_count; const TypeMapEntry *java_to_managed; const TypeMapEntry *managed_to_java; }; - -// MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs -struct TypeMapAssembly -{ - uint8_t module_uuid[16]; - uint64_t name_length; - uint64_t name_offset; // into the assembly names blob -}; #else struct TypeMapModuleEntry { @@ -295,7 +286,6 @@ extern "C" { #if defined (DEBUG) [[gnu::visibility("default")]] extern const TypeMap type_map; // MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs [[gnu::visibility("default")]] extern const TypeMapManagedTypeInfo type_map_managed_type_info[]; - [[gnu::visibility("default")]] extern const TypeMapAssembly type_map_unique_assemblies[]; [[gnu::visibility("default")]] extern const char type_map_assembly_names[]; [[gnu::visibility("default")]] extern const char type_map_managed_type_names[]; [[gnu::visibility("default")]] extern const char type_map_java_type_names[]; diff --git a/src/native/clr/xamarin-app-stub/application_dso_stub.cc b/src/native/clr/xamarin-app-stub/application_dso_stub.cc index 598418e3235..da3b6c4131c 100644 --- a/src/native/clr/xamarin-app-stub/application_dso_stub.cc +++ b/src/native/clr/xamarin-app-stub/application_dso_stub.cc @@ -14,13 +14,11 @@ static TypeMapEntry managed_to_java[] = {}; // MUST match src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGenerator.cs const TypeMap type_map = { .entry_count = 0, - .unique_assemblies_count = 0, .java_to_managed = java_to_managed, .managed_to_java = managed_to_java, }; const TypeMapManagedTypeInfo type_map_managed_type_info[] = {}; -const TypeMapAssembly type_map_unique_assemblies[] = {}; const char type_map_assembly_names[] = {}; const char type_map_managed_type_names[] = {}; const char type_map_java_type_names[] = {}; diff --git a/src/native/nativeaot/host/internal-pinvoke-stubs.cc b/src/native/nativeaot/host/internal-pinvoke-stubs.cc index 1c59786a9b1..1e7dd83833b 100644 --- a/src/native/nativeaot/host/internal-pinvoke-stubs.cc +++ b/src/native/nativeaot/host/internal-pinvoke-stubs.cc @@ -18,6 +18,7 @@ namespace { const char* clr_typemap_managed_to_java ( [[maybe_unused]] const char *typeName, + [[maybe_unused]] const char *assemblyFullName, [[maybe_unused]] const uint8_t *mvid) noexcept { pinvoke_unreachable (); diff --git a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs index 46fa0f491be..249b9d2bed0 100644 --- a/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/FastDevTest.cs @@ -39,44 +39,103 @@ public void FastDevSimpleBuild () } [Test] - public void FastDeployUpdatesTypeMapAfterAssemblyEdit () + [TestCase ("FastDeploy")] + [TestCase ("FastDeploy2")] + public void FastDeployUpdatesTypeMapAfterAssemblyEdit (string strategy) { + const string logcatMessage = "FAST_DEPLOY_TYPEMAP_TEST_MESSAGE"; + var proj = new XamarinAndroidApplicationProject { - PackageName = "com.xamarin.fastdeploy_typemap", + PackageName = $"com.xamarin.fastdeploy_typemap_{strategy.ToLowerInvariant ()}", }; proj.SetDefaultTargetDevice (); - proj.SetProperty ("_AndroidFastDevStrategy", "FastDeploy"); + proj.SetProperty ("_AndroidFastDevStrategy", strategy); + + // 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. + var typeMapTargets = new [] { + "_GenerateJavaStubs", + "_CompileJava", + "_CompileToDalvik", + "_CompileNativeAssemblySources", + "_CreateApplicationSharedLibraries", + "_BuildApkFastDev", + "_Sign", + }; + + using var builder = CreateApkBuilder (); + // `IsApkInstalled` requires detailed verbosity, see its documentation. + builder.Verbosity = LoggerVerbosity.Detailed; + + // 1. Initial build and deployment, with only MainActivity. + 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"); + + // 2. A C#-only change that adds a new Java-callable type, so the type map *must* be updated. proj.MainActivity = proj.DefaultMainActivity .Replace ("//${AFTER_ONCREATE}", "StartActivity (new Android.Content.Intent (this, typeof (SecondActivity)));") .Replace ("//${AFTER_MAINACTIVITY}", """ [Activity (Label = "Fast Deploy Result")] public sealed class SecondActivity : Activity { + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + //${SECOND_ACTIVITY_ONCREATE} + } } """); + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "Install of the new activity should have succeeded."); - using var builder = CreateApkBuilder (); - Assert.IsTrue (builder.Install (proj), "Initial install should have succeeded."); - AssertSecondActivityStarts ("initial-launch.log"); + builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 2); + foreach (var target in typeMapTargets) { + builder.Output.AssertTargetIsNotSkipped (target, occurrence: 2); + } + Assert.IsTrue (builder.Output.IsApkInstalled, "The .apk should have been reinstalled after adding a new activity."); + AssertActivityStarts ("SecondActivity", "new-activity-launch.log"); - proj.MainActivity += "// Incremental C# edit."; + // 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. + 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."); - AssertSecondActivityStarts ("incremental-launch.log"); + + builder.Output.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 3); + foreach (var target in new [] { "_CompileNativeAssemblySources", "_CreateApplicationSharedLibraries", "_BuildApkFastDev", "_Sign" }) { + builder.Output.AssertTargetIsSkipped (target, occurrence: 3); + } + Assert.IsFalse (builder.Output.IsApkInstalled, "The .apk should not be reinstalled for a C#-only change."); + + ClearAdbLogcat (); + Assert.IsTrue ( + MonitorAdbLogcat ( + line => line.Contains (logcatMessage), + Path.Combine (Root, builder.ProjectDirectory, "incremental-launch.log"), + ActivityStartTimeoutInSeconds, + onMonitoringStarted: () => { + AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity"); + } + ), + $"`{logcatMessage}` should have been logged by the fast deployed assembly." + ); + Assert.IsTrue (builder.Uninstall (proj), "Uninstall should have succeeded."); - void AssertSecondActivityStarts (string logFileName) + void AssertActivityStarts (string activityName, string logFileName) { ClearAdbLogcat (); AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity"); Assert.IsTrue ( WaitForActivityToStart ( proj.PackageName, - "SecondActivity", + activityName, Path.Combine (Root, builder.ProjectDirectory, logFileName), ActivityStartTimeoutInSeconds ), - "SecondActivity should have started." + $"{activityName} should have started." ); } } @@ -183,7 +242,7 @@ public void SkipFastDevAlreadyInstalledFile () proj.Touch ("MainActivity.cs"); // make sure that the fastdev log tells that the relevant dll is updated but NOT for others. Assert.IsTrue (b.Install (proj, doNotCleanupOnUpdate: true, saveProject: false), "install should have succeeded."); - Assert.IsTrue (b.Output.IsApkInstalled, "app apk was not installed"); + Assert.IsFalse (b.Output.IsApkInstalled, "app apk was reinstalled"); Assert.IsTrue (b.LastBuildOutput.Any (l => l.Contains ("UnnamedProject.dll") && l.Contains ("NotifySync CopyFile")), "app dll not uploaded"); var assemblies = new[] {