Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,22 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
string jniName = kvp.Key;
var peersForName = kvp.Value;

// Sort aliases by managed type name for deterministic proxy naming
// Order aliases to match the java→managed selection the native runtime performs (see
// clr_typemap_java_to_managed / monovm_typemap_java_to_managed and NativeTypeMappingData):
// the runtime builds its java→managed map by processing the Mono.Android module first and
// keeping the first managed type that claims a Java name (first-writer-wins). GetTypeForSimpleReference
// returns the first (index [0]) alias, so put the Mono.Android peer first — e.g. java/lang/Object
// must resolve to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject. Remaining peers are
// ordered by ordinal managed type name for deterministic proxy naming.
if (peersForName.Count > 1) {
peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
peersForName.Sort ((a, b) => {
bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal);
bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal);
if (aMonoAndroid != bMonoAndroid) {
return aMonoAndroid ? -1 : 1;
}
return StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName);
});
}

EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

Expand Down Expand Up @@ -107,12 +108,28 @@ public void WritePE (Stream stream)
new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
new MetadataRootBuilder (Metadata),
ILBuilder,
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null);
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null,
// Derive the PE content id (and thus the header TimeDateStamp/debug id) from a hash of the
// image so the bytes are fully deterministic. Without this, ManagedPEBuilder falls back to a
// time-based id, so every regeneration produces different bytes even for identical input; that
// churns the generated typemap assemblies and breaks incremental packaging (a no-op rebuild
// re-touches the .dll, forcing repackage + re-sign). The MVID is already deterministic.
deterministicIdProvider: DeterministicContentId);
var peBlob = new BlobBuilder ();
peBuilder.Serialize (peBlob);
peBlob.WriteContentTo (stream);
}

static BlobContentId DeterministicContentId (IEnumerable<Blob> content)
{
using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256);
foreach (var blob in content) {
var segment = blob.GetBytes ();
hash.AppendData (segment.Array!, segment.Offset, segment.Count);
}
Comment on lines +125 to +129

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we think this won't ever be null, could we put Debug.Assert(segment.Array is not null)?

return BlobContentId.FromHash (hash.GetHashAndReset ());
}

/// <summary>
/// Adds (or retrieves from cache) an assembly reference.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface ITrimmableTypeMapLogger
void LogGeneratedJcwFilesInfo (int sourceCount);
void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName);
void LogManifestReferencedTypeNotFoundWarning (string javaTypeName);
void LogInvalidManifestPlaceholderWarning (string placeholders);
void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
// with the wrong AttributeName.
List<IntentFilterInfo>? intentFilters = null;
List<MetaDataInfo>? metaData = null;
Dictionary<string, object?>? layoutProperties = null;

foreach (var caHandle in typeDef.GetCustomAttributes ()) {
var ca = Reader.GetCustomAttribute (caHandle);
Expand Down Expand Up @@ -214,6 +215,8 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
metaData ??= new List<MetaDataInfo> ();
var (mdName, mdProps) = ParseNameAndProperties (ca);
metaData.Add (CreateMetaDataInfo (mdName, mdProps));
} else if (attrName == "LayoutAttribute") {
layoutProperties = ParseLayoutAttribute (ca);
} else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) {
// Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName])
var name = TryGetNameProperty (ca);
Expand All @@ -232,6 +235,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
if (metaData is not null) {
attrInfo.MetaData.AddRange (metaData);
}
if (layoutProperties is not null) {
attrInfo.LayoutProperties = layoutProperties;
}
}

return (registerInfo, attrInfo);
Expand Down Expand Up @@ -425,6 +431,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue<string> value)
return null;
}

Dictionary<string, object?> ParseLayoutAttribute (CustomAttribute ca)
{
var value = DecodeAttribute (ca);
var properties = new Dictionary<string, object?> (StringComparer.Ordinal);
foreach (var named in value.NamedArguments) {
if (named.Name is not null) {
properties [named.Name] = named.Value;
}
}
return properties;
}

IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca)
{
var value = DecodeAttribute (ca);
Expand Down Expand Up @@ -712,6 +730,12 @@ class TypeAttributeInfo (string attributeName)
/// Metadata entries declared on this type via [MetaData] attributes.
/// </summary>
public List<MetaDataInfo> MetaData { get; } = [];

/// <summary>
/// Named property values from a [Layout] attribute on this type, or null if none.
/// Maps to the &lt;layout&gt; child element of the component in the manifest.
/// </summary>
public Dictionary<string, object?>? LayoutProperties { get; set; }
}

sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string
}
}

// Managed types that must never be emitted into the trimmable type map, keyed by
// (managed full name, assembly simple name). These are reflection-based ([RequiresUnreferencedCode])
// helpers that the trimmable runtime never activates via the type map; emitting proxies for them
// only produces IL2026 trim warnings.
static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) =>
managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop";

void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results)
{
foreach (var typeHandle in index.Reader.TypeDefinitions) {
Expand All @@ -286,6 +293,16 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A

var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader);

// Java.Interop.ManagedPeer is a reflection-based helper (marked
// [RequiresUnreferencedCode]) that is not supported by the trimmable type map: on the
// trimmable path native registration goes through IAndroidCallableWrapper.RegisterNatives
// and ManagedPeerNativeRegistration is disabled, so ManagedPeer is never activated via the
// type map. Emitting a proxy for it only produces IL2026 trim warnings (its constructors
// use reflection), so exclude it here.
if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) {
continue;
}

// Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate
// which scenarios fail later in the trimmable typemap pipeline.
// if (index.MayUseJniAddNativeMethodRegistrationAttribute &&
Expand Down Expand Up @@ -2528,6 +2545,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<J
Properties = attrInfo.Properties,
IntentFilters = attrInfo.IntentFilters,
MetaData = attrInfo.MetaData,
LayoutProperties = attrInfo.LayoutProperties,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
ForceExtractNativeLibs = forceDebuggable,
ManifestPlaceholders = config.ManifestPlaceholders,
ApplicationJavaClass = config.ApplicationJavaClass,
WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders),
};

var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName);
public void LogInvalidManifestPlaceholderWarning (string placeholders) =>
log.LogCodedWarning ("XA1010", Properties.Resources.XA1010, placeholders);
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type.");
public void LogInvalidManifestPlaceholderWarning (string placeholders) =>
warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'.");
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,29 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries ()
Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count);
}

[Fact]
public void Build_AliasGroup_MonoAndroidPeerSortsFirst ()
{
// Mirror the native runtime's java→managed selection (clr_typemap_java_to_managed /
// monovm_typemap_java_to_managed): NativeTypeMappingData builds that map by processing the
// Mono.Android module first and keeping the first managed type to claim a Java name, so
// java/lang/Object resolves to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject
// (Java.Interop). GetTypeForSimpleReference returns alias index [0], so the Mono.Android peer
// must sort first. Declare them in the "wrong" order to prove the sort reorders them.
var peers = new List<JavaPeerInfo> {
MakeMcwPeer ("java/lang/Object", "Java.Interop.JavaObject", "Java.Interop") with { IsFromJniTypeSignature = true },
MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"),
};

var model = BuildModel (peers, "MonoAndroid");

// The Mono.Android peer (Java.Lang.Object) must be alias index [0].
Assert.Equal ("java/lang/Object[0]", model.Entries [0].MapKey);
Assert.Contains ("Java.Lang.Object", model.Entries [0].ProxyTypeReference);
Assert.Equal ("java/lang/Object[1]", model.Entries [1].MapKey);
Assert.Contains ("Java.Interop.JavaObject", model.Entries [1].ProxyTypeReference);
}

[Fact]
public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivation ()
{
Expand Down