Skip to content

Commit ed92bec

Browse files
[TrimmableTypeMap] Generator correctness fixes (#11988)
Java.Interop.ManagedPeer is a reflection-based helper marked [RequiresUnreferencedCode] ("Uses reflection to find constructors and invoke them."). The trimmable type map generator emitted a proxy for it (_TypeMap.Proxies.Java_Interop_ManagedPeer_Proxy) whose constructor references ManagedPeer's constructors, producing two IL2026 trim warnings, aggregated by ILC into: IL2104: Assembly '_Java.Interop.TypeMap' produced trim warnings Because the default NativeAOT type map is now trimmable, this surfaced as a real MSBuild warning and broke NativeAOT tests that assert no warnings (e.g. BuildWithJavaToolOptions). ManagedPeer 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. Exclude it in the scanner so no proxy is emitted. Verified on an arm64 emulator: the HelloWorld NativeAOT (trimmable) sample now builds with 0 warnings and still launches to MainActivity. ### [TrimmableTypeMap] Emit <layout> element for the [Layout] attribute The trimmable manifest generator already had ComponentElementBuilder support for a <layout> child element, but the scanner never populated ComponentInfo.LayoutProperties, so [Layout(...)] on an activity produced no <layout> element (LayoutAttributeElement failed on NativeAOT). Parse the [Layout] attribute's named properties in AssemblyIndex.ParseAttributes (collected separately to tolerate attribute ordering, like [IntentFilter] and [MetaData]) and flow them through TypeAttributeInfo.LayoutProperties into ComponentInfo.LayoutProperties. ### [TrimmableTypeMap] Emit XA1010 for invalid $(AndroidManifestPlaceholders) ManifestGenerator.ApplyPlaceholders already invoked a WarnInvalidPlaceholder callback for placeholder entries without '=', but the callback was never wired up, so the trimmable path silently dropped the XA1010 warning the legacy ManifestDocument emits (ManifestPlaceHoldersXA1010 failed on NativeAOT). Add ITrimmableTypeMapLogger.LogInvalidManifestPlaceholderWarning (logging XA1010 from the MSBuild logger) and wire the ManifestGenerator instance's WarnInvalidPlaceholder to it. The rooting-only PrepareManifestForRooting pass stays silent so the warning is not emitted twice. ### [TrimmableTypeMap] Make generated typemap assemblies byte-deterministic PEAssemblyBuilder.WritePE let ManagedPEBuilder fall back to a time-based PE content id, so every regeneration of a typemap assembly produced different bytes (different PE TimeDateStamp) even for identical input — the MVID was already deterministic, but the image was not. That churn broke incremental packaging on NativeAOT: an SDK CoreCompile rerun (e.g. touching a .csproj.user file, which the SDK treats as a compile input) rewrites the app assembly, reruns _GenerateTrimmableTypeMap, and — because the regenerated *.TypeMap.dll got a fresh timestamp — forced _BuildApkEmbed to repackage and _Sign to re-sign (CSProjUserFileChanges failed on NativeAOT). Supply a deterministic content-id provider (SHA-256 over the serialized image) so identical input yields byte-identical output; CopyIfStreamChanged then keeps the existing file/timestamp and downstream targets stay incremental. ### [TrimmableTypeMap] Resolve java/lang/Object to Java.Lang.Object For a JNI name mapped by multiple managed types (an alias group), the trimmable type map's GetTypeForSimpleReference returns the first (index [0]) alias. Order the aliases to match the native runtime's java->managed selection (clr_typemap_java_to_managed / monovm_typemap_java_to_managed, built by NativeTypeMappingData): the Mono.Android module is processed first and the first managed type to claim a Java name wins. So java/lang/Object must resolve to Java.Lang.Object (Mono.Android), not Java.Interop.JavaObject (Java.Interop). Order alias peers Mono.Android-assembly-first, with an ordinal managed-name tiebreak for deterministic proxy naming.
1 parent 6771716 commit ed92bec

12 files changed

Lines changed: 128 additions & 6 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ static class ComponentElementBuilder
4444
element.Add (CreateIntentFilterElement (intentFilter));
4545
}
4646

47-
// Add <layout> element from a [Layout] attribute, if present
48-
if (component.LayoutProperties is not null) {
47+
if (component.Kind == ComponentKind.Activity && component.LayoutProperties is not null) {
4948
var layout = CreateLayoutElement (component.LayoutProperties);
5049
if (layout is not null) {
5150
element.Add (layout);

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ internal static void ApplyPlaceholders (XDocument doc, string? placeholders, str
516516
if (!placeholders.IsNullOrEmpty ()) {
517517
foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) {
518518
var eqIndex = entry.IndexOf ('=');
519-
if (eqIndex > 0) {
519+
if (eqIndex >= 0) {
520520
var key = entry.Substring (0, eqIndex).Trim ();
521521
var value = entry.Substring (eqIndex + 1).Trim ();
522522
replacements ["${" + key + "}"] = value;

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
102102
string jniName = kvp.Key;
103103
var peersForName = kvp.Value;
104104

105-
// Sort aliases by managed type name for deterministic proxy naming
106105
if (peersForName.Count > 1) {
107-
peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
106+
peersForName.Sort (CompareAliasesForRuntimeResolution);
108107
}
109108

110109
EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
@@ -141,6 +140,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
141140
return model;
142141
}
143142

143+
static int CompareAliasesForRuntimeResolution (JavaPeerInfo a, JavaPeerInfo b)
144+
{
145+
// Keep alias [0] aligned with the native java→managed map, which processes Mono.Android first.
146+
bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal);
147+
bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal);
148+
if (aMonoAndroid != bMonoAndroid) {
149+
return aMonoAndroid ? -1 : 1;
150+
}
151+
152+
int result = StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName);
153+
return result != 0 ? result : StringComparer.Ordinal.Compare (a.AssemblyName, b.AssemblyName);
154+
}
155+
144156
static void EmitPeers (TypeMapAssemblyData model, string jniName,
145157
List<JavaPeerInfo> peersForName, string assemblyName, HashSet<string> usedProxyNames)
146158
{

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.IO;
45
using System.Linq;
56
using System.Reflection;
67
using System.Reflection.Metadata;
78
using System.Reflection.Metadata.Ecma335;
89
using System.Reflection.PortableExecutable;
10+
using System.Security.Cryptography;
911

1012
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
1113

@@ -107,12 +109,28 @@ public void WritePE (Stream stream)
107109
new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll),
108110
new MetadataRootBuilder (Metadata),
109111
ILBuilder,
110-
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null);
112+
mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null,
113+
// ManagedPEBuilder otherwise uses a time-based id, changing bytes on every regeneration.
114+
deterministicIdProvider: DeterministicContentId);
111115
var peBlob = new BlobBuilder ();
112116
peBuilder.Serialize (peBlob);
113117
peBlob.WriteContentTo (stream);
114118
}
115119

120+
static BlobContentId DeterministicContentId (IEnumerable<Blob> content)
121+
{
122+
using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256);
123+
foreach (var blob in content) {
124+
var segment = blob.GetBytes ();
125+
if (segment.Count == 0) {
126+
continue;
127+
}
128+
Debug.Assert (segment.Array is not null);
129+
hash.AppendData (segment.Array, segment.Offset, segment.Count);
130+
}
131+
return BlobContentId.FromHash (hash.GetHashAndReset ());
132+
}
133+
116134
/// <summary>
117135
/// Adds (or retrieves from cache) an assembly reference.
118136
/// </summary>

src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public interface ITrimmableTypeMapLogger
1313
void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName);
1414
void LogManifestReferencedTypeNotFoundWarning (string javaTypeName);
1515
void LogLibraryManifestMergeWarning (string message);
16+
void LogInvalidManifestPlaceholderWarning (string placeholders);
1617
void LogUnresolvableJavaPeerSkippedWarning (
1718
string managedTypeName,
1819
string assemblyName,

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
164164
// with the wrong AttributeName.
165165
List<IntentFilterInfo>? intentFilters = null;
166166
List<MetaDataInfo>? metaData = null;
167+
Dictionary<string, object?>? layoutProperties = null;
167168

168169
foreach (var caHandle in typeDef.GetCustomAttributes ()) {
169170
var ca = Reader.GetCustomAttribute (caHandle);
@@ -214,6 +215,8 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
214215
metaData ??= new List<MetaDataInfo> ();
215216
var (mdName, mdProps) = ParseNameAndProperties (ca);
216217
metaData.Add (CreateMetaDataInfo (mdName, mdProps));
218+
} else if (attrName == "LayoutAttribute") {
219+
layoutProperties = ParseLayoutAttribute (ca);
217220
} else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) {
218221
// Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName])
219222
var name = TryGetNameProperty (ca);
@@ -232,6 +235,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen
232235
if (metaData is not null) {
233236
attrInfo.MetaData.AddRange (metaData);
234237
}
238+
if (layoutProperties is not null) {
239+
attrInfo.LayoutProperties = layoutProperties;
240+
}
235241
}
236242

237243
return (registerInfo, attrInfo);
@@ -425,6 +431,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue<string> value)
425431
return null;
426432
}
427433

434+
Dictionary<string, object?> ParseLayoutAttribute (CustomAttribute ca)
435+
{
436+
var value = DecodeAttribute (ca);
437+
var properties = new Dictionary<string, object?> (StringComparer.Ordinal);
438+
foreach (var named in value.NamedArguments) {
439+
if (named.Name is not null) {
440+
properties [named.Name] = named.Value;
441+
}
442+
}
443+
return properties;
444+
}
445+
428446
IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca)
429447
{
430448
var value = DecodeAttribute (ca);
@@ -712,6 +730,12 @@ class TypeAttributeInfo (string attributeName)
712730
/// Metadata entries declared on this type via [MetaData] attributes.
713731
/// </summary>
714732
public List<MetaDataInfo> MetaData { get; } = [];
733+
734+
/// <summary>
735+
/// Named property values from a [Layout] attribute on this type, or null if none.
736+
/// Maps to the &lt;layout&gt; child element of the component in the manifest.
737+
/// </summary>
738+
public Dictionary<string, object?>? LayoutProperties { get; set; }
715739
}
716740

717741
sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute")

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string
274274
}
275275
}
276276

277+
// ManagedPeer depends on reflection-based registration; the trimmable path uses IAndroidCallableWrapper.
278+
static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) =>
279+
managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop";
280+
277281
void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results)
278282
{
279283
foreach (var typeHandle in index.Reader.TypeDefinitions) {
@@ -286,6 +290,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
286290

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

293+
if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) {
294+
continue;
295+
}
296+
289297
// Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate
290298
// which scenarios fail later in the trimmable typemap pipeline.
291299
// if (index.MayUseJniAddNativeMethodRegistrationAttribute &&
@@ -2528,6 +2536,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<J
25282536
Properties = attrInfo.Properties,
25292537
IntentFilters = attrInfo.IntentFilters,
25302538
MetaData = attrInfo.MetaData,
2539+
LayoutProperties = attrInfo.LayoutProperties,
25312540
};
25322541
}
25332542
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
155155
// Other codes (e.g. unresolvable type properties) are not yet assigned XA codes
156156
// and are intentionally not surfaced here.
157157
},
158+
WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders),
158159
LibraryManifests = config.LibraryManifests ?? [],
159160
};
160161

src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
4646
log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName);
4747
public void LogLibraryManifestMergeWarning (string message) =>
4848
log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message);
49+
public void LogInvalidManifestPlaceholderWarning (string placeholders) =>
50+
log.LogCodedWarning ("XA1010", Properties.Resources.XA1010, placeholders);
4951
public void LogUnresolvableJavaPeerSkippedWarning (
5052
string managedTypeName,
5153
string assemblyName,

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ public void Placeholders_AllValid_DoesNotWarn ()
105105
Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label"));
106106
}
107107

108+
[Fact]
109+
public void Placeholders_EmptyKey_ReplacesEmptyTokenWithoutWarning ()
110+
{
111+
var gen = CreateDefaultGenerator ();
112+
var warnings = new List<string> ();
113+
gen.WarnInvalidPlaceholder = warnings.Add;
114+
gen.ManifestPlaceholders = "=val1";
115+
var template = ParseTemplate ("""
116+
<?xml version="1.0" encoding="utf-8"?>
117+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
118+
<uses-sdk />
119+
<application android:label="${}" />
120+
</manifest>
121+
""");
122+
var doc = GenerateAndLoad (gen, template: template);
123+
Assert.Empty (warnings);
124+
Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label"));
125+
}
126+
108127
[Fact]
109128
public void Package_PlaceholderToken_ReplacedWithResolvedPackageName ()
110129
{
@@ -385,6 +404,24 @@ public void Activity_LayoutAttributeElement ()
385404
Assert.Equal ("400dp", (string?)layout?.Attribute (AndroidNs + "minHeight"));
386405
}
387406

407+
[Fact]
408+
public void Service_LayoutAttributeElement_Ignored ()
409+
{
410+
var gen = CreateDefaultGenerator ();
411+
var peer = CreatePeer ("com/example/app/MyService", new ComponentInfo {
412+
Kind = ComponentKind.Service,
413+
LayoutProperties = new Dictionary<string, object?> {
414+
["DefaultWidth"] = "500dp",
415+
},
416+
});
417+
418+
var doc = GenerateAndLoad (gen, [peer]);
419+
var service = doc.Root?.Element ("application")?.Element ("service");
420+
421+
Assert.NotNull (service);
422+
Assert.Null (service?.Element ("layout"));
423+
}
424+
388425
[Fact]
389426
public void Activity_AllExtendedProperties ()
390427
{

0 commit comments

Comments
 (0)