Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>(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<byte>(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;
Expand All @@ -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");
}
Expand Down
2 changes: 1 addition & 1 deletion src/Mono.Android/Android.Runtime/RuntimeNativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) })]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ This file contains the CoreCLR-specific MSBuild logic for .NET for Android.
Value="false"
Trim="true"
/>
<RuntimeHostConfigurationOption Include="Microsoft.Android.Runtime.RuntimeFeature.ManagedToJavaUsesAssemblyFullName"
Value="$(AndroidIncludeDebugSymbols)"
Trim="true"
/>
</ItemGroup>

<Target Name="_CLRUseLocalRuntimePacks" AfterTargets="ResolveFrameworkReferences"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,11 @@ string GenerateStubLlvmIr (string abi)

if (Debug) {
return header + """
%struct.TypeMap = type { i32, i32, ptr, ptr }
%struct.TypeMapManagedTypeInfo = type { i64, i32, i32 }
%struct.TypeMapAssembly = type { [16 x i8], i64, i64 }
%struct.TypeMap = type { i32, ptr, ptr }
%struct.TypeMapManagedTypeInfo = type { i32, i32 }

@type_map = dso_local constant %struct.TypeMap zeroinitializer, align 8
@type_map_managed_type_info = dso_local constant [0 x %struct.TypeMapManagedTypeInfo] zeroinitializer, align 8
@type_map_unique_assemblies = dso_local constant [0 x %struct.TypeMapAssembly] zeroinitializer, align 8
@type_map_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1
@type_map_managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1
@type_map_java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,32 @@ public void GenerateJavaStubsAndAssembly ([Values] bool isRelease, [Values (Andr
Assert.IsTrue (b.Output.IsTargetSkipped (target), $"`{target}` should be skipped!");
}
AssertAssemblyFilesInFileWrites (proj, b, abi, runtime);

if (!isRelease && runtime == AndroidRuntime.CoreCLR) {
string projectDirectory = Path.Combine (Root, b.ProjectDirectory);
string intermediate = Path.Combine (projectDirectory, proj.IntermediateOutputPath, MonoAndroidHelper.AbiToRid (abi));
string typemap = Path.Combine (intermediate, "android", $"typemaps.{abi}.ll");
string apk = Directory.GetFiles (Path.Combine (projectDirectory, proj.OutputPath), "*-Signed.apk", SearchOption.AllDirectories).Single ();
DateTime typemapWriteTime = File.GetLastWriteTimeUtc (typemap);
DateTime apkWriteTime = File.GetLastWriteTimeUtc (apk);
string typemapHash = Files.HashFile (typemap);
string apkHash = Files.HashFile (apk);

// Change managed code without changing any Java type mappings.
proj.MainActivity = proj.MainActivity.Replace ("clicks", "CLICKS");
proj.Touch ("MainActivity.cs");
Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "fourth build should have succeeded.");

b.Output.AssertTargetIsNotSkipped ("CoreCompile");
b.Output.AssertTargetIsSkipped ("_CompileNativeAssemblySources");
b.Output.AssertTargetIsSkipped ("_CreateApplicationSharedLibraries");
b.Output.AssertTargetIsSkipped ("_BuildApkFastDev");
b.Output.AssertTargetIsSkipped ("_Sign");
Assert.AreEqual (typemapWriteTime, File.GetLastWriteTimeUtc (typemap), $"{typemap} should not be rewritten when its mappings have not changed.");
Assert.AreEqual (typemapHash, Files.HashFile (typemap), $"{typemap} contents should not change.");
Assert.AreEqual (apkWriteTime, File.GetLastWriteTimeUtc (apk), $"{apk} should not be rewritten for an incremental C# change.");
Assert.AreEqual (apkHash, Files.HashFile (apk), $"{apk} contents should not change.");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Utilities;
using Mono.Cecil;
using NUnit.Framework;
using Xamarin.Android.Tasks;
using Xamarin.Android.Tasks.LLVMIR;
using Xamarin.Android.Tools;

Expand Down Expand Up @@ -79,5 +81,27 @@ public void GeneratedIR_FunctionWithWhitespaceParameterName_ProducesValidOutput
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");
}

[Test]
public void TypeMapAssemblyFullNameUsesRuntimeEscaping ()
{
var assemblyName = new AssemblyNameDefinition ("Comma,Name", new Version (1, 2, 3, 4));

Assert.That (
TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assemblyName),
Is.EqualTo (@"Comma\,Name, Version=1.2.3.4, Culture=neutral, PublicKeyToken=null")
);
}

[Test]
public void TypeMapAssemblyFullNameMatchesStrongNamedRuntimeAssembly ()
{
string assemblyPath = typeof (TypeMapCecilAdapter).Assembly.Location;
using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath);
string? runtimeFullName = System.Reflection.AssemblyName.GetAssemblyName (assemblyPath).FullName;

Assert.That (runtimeFullName, Does.Not.EndWith ("PublicKeyToken=null"));
Assert.That (TypeMapCecilAdapter.GetRuntimeAssemblyFullName (assembly.Name), Is.EqualTo (runtimeFullName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Xamarin.Tools.Zip;

namespace Xamarin.ProjectTools
Expand Down Expand Up @@ -170,8 +171,27 @@ public TimeSpan GetTargetOrTaskTime (string targetOrTask)
return TimeSpan.Zero;
}

/// <summary>
/// Gets a value indicating whether the .apk was installed on the device by the last build.
/// </summary>
/// <remarks>
/// <para>This scans the build output for the <c>Installed Package</c> message written by the
/// <c>FastDeploy</c> and <c>FastDeploy2</c> tasks. Both write it with
/// <see cref="Microsoft.Build.Framework.MessageImportance.Low"/>, so it is only present in the
/// build output at <see cref="Microsoft.Build.Framework.LoggerVerbosity.Detailed"/> or higher.</para>
/// <para>At a lower verbosity this property would silently return <c>false</c> no matter what the
/// build did, which makes both <c>Assert.IsTrue</c> and <c>Assert.IsFalse</c> 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.</para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// <see cref="Builder"/> is running at a verbosity below
/// <see cref="Microsoft.Build.Framework.LoggerVerbosity.Detailed"/>.
/// </exception>
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}'.");
Comment thread
jonathanpeppers marked this conversation as resolved.
}

static string GetManagedTypeName (TypeDefinition td)
{
// This is necessary because Mono runtime will return to us type name with a `.` for nested types (not a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeMapDebugEntry> JavaToManagedDebugEntries { get; } = [];
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -183,13 +185,15 @@ 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);

var file = new TypeMapObjectsXmlFile {
WasScanned = true,
AssemblyName = assemblyName,
AssemblyFullName = assemblyFullName,
AssemblyMvid = mvid,
FoundJniNativeRegistration = foundJniNativeRegistration,
};
Expand All @@ -205,16 +209,17 @@ 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 ()) {
if (reader.NodeType != XmlNodeType.Element)
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);
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -278,6 +283,7 @@ static TypeMapDebugEntry FromDebugEntryXml (XmlReader reader, string assemblyNam
IsInvoker = GetAttributeOrDefault (reader, "is-invoker", false),
IsMonoAndroid = isMonoAndroid,
AssemblyName = assemblyName,
AssemblyFullName = assemblyFullName,
};
}

Expand All @@ -301,7 +307,7 @@ static T GetAttributeOrDefault<T> (XmlReader reader, string name, T defaultValue
return (T) Convert.ChangeType (value, typeof (T), CultureInfo.InvariantCulture);
}

static void ReadDebugEntries (XmlReader reader, List<TypeMapDebugEntry> entries, string assemblyName, bool isMonoAndroid)
static void ReadDebugEntries (XmlReader reader, List<TypeMapDebugEntry> entries, string assemblyName, string assemblyFullName, bool isMonoAndroid)
{
if (reader.IsEmptyElement)
return;
Expand All @@ -313,7 +319,7 @@ static void ReadDebugEntries (XmlReader reader, List<TypeMapDebugEntry> entries,
return;

if (reader.NodeType == XmlNodeType.Element && reader.Name == "entry")
entries.Add (FromDebugEntryXml (reader, assemblyName, isMonoAndroid));
entries.Add (FromDebugEntryXml (reader, assemblyName, assemblyFullName, isMonoAndroid));
}
}

Expand Down
Loading
Loading