Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d8b412d
[Mono.Android] Trimmable typemap: fix JavaCast/JavaAs behavior
simonrozsival Apr 28, 2026
6fd563f
[Tests] Re-enable 7 trimmable typemap tests
simonrozsival Apr 28, 2026
e197f36
[TrimmableTypeMap] Generator: emit per-rank array TypeMap entries
simonrozsival Apr 29, 2026
f73f2b6
[TrimmableTypeMap] Runtime: array typemap fork on IsDynamicCodeSupported
simonrozsival Apr 29, 2026
d6bfcc6
[TrimmableTypeMap] Address PR feedback
simonrozsival Apr 29, 2026
af31ed3
[TrimmableTypeMap] Trim verbose doc comments
simonrozsival Apr 29, 2026
961cb87
[TrimmableTypeMap] Wire up merged-universe + array entries path
simonrozsival Apr 29, 2026
60cbadd
[TrimmableTypeMap] Tighten comments in shared+arrays IL emit
simonrozsival Apr 29, 2026
1c89cdd
[TrimmableTypeMap] Reject multi-dim arrays in TryGetArrayType
simonrozsival Apr 29, 2026
56350ce
[TrimmableTypeMap] Inline rank sentinel name generation
simonrozsival Apr 29, 2026
36ad0f5
[TrimmableTypeMap] Drop CompositeStringTypeReadOnlyDictionary
simonrozsival Apr 29, 2026
926648c
[TrimmableTypeMap] Use shared rank anchors in Mono.Android
simonrozsival Apr 29, 2026
4e112e5
[TrimmableTypeMap] Simplify TryGetArrayType
simonrozsival Apr 29, 2026
50ccdfd
[TrimmableTypeMap] Unify Initialize IL emit paths
simonrozsival Apr 29, 2026
b331a4d
[TrimmableTypeMap] Two small simplifications
simonrozsival Apr 29, 2026
7015f5e
[TrimmableTypeMap] Fold model setup into a single object initializer
simonrozsival Apr 29, 2026
ddfb16b
[TrimmableTypeMap] Drop stale 'TypeDef was emitted' check + message
simonrozsival Apr 29, 2026
bc80972
Address trimmable array typemap review feedback
simonrozsival May 3, 2026
d7ab389
Fix xUnit2031: use Assert.Single with predicate instead of Where+Asse…
Copilot May 4, 2026
340aa89
Re-enable trimmable array runtime tests
simonrozsival May 5, 2026
4c6a72e
[NativeAOT] Initialize trimmable typemap runtime
simonrozsival May 5, 2026
cdc3180
[NativeAOT] Use conditional trimmable typemap entries
simonrozsival May 5, 2026
8f61332
[NativeAOT] Avoid exporting framework typemap assemblies
simonrozsival May 5, 2026
d78f7d1
[NativeAOT] Simplify trimmable typemap configuration
simonrozsival May 5, 2026
fe486cf
[NativeAOT] Handle GC peer class lookup failures
simonrozsival May 5, 2026
af0ff31
[TrimmableTypeMap] Treat framework ACWs as conditional
simonrozsival May 5, 2026
aa91d95
[TrimmableTypeMap] Compile array map anchors
simonrozsival May 5, 2026
5d45788
[TrimmableTypeMap] Remove inclusion decision diagnostics
simonrozsival May 6, 2026
13998cf
[NativeAOT] Inline array map anchors
simonrozsival May 6, 2026
1c5a767
[TrimmableTypeMap] Move force-unconditional flag to generator state
simonrozsival May 6, 2026
6e467f5
[NativeAOT] Split CoreCLR and NativeAOT runtime branches
simonrozsival May 6, 2026
ad2a115
[TrimmableTypeMap] Remove assembly summary logging
simonrozsival May 6, 2026
41c4ceb
[TrimmableTypeMap] Use type map assembly generator wrapper
simonrozsival May 6, 2026
62e7313
[TrimmableTypeMap] Simplify assembly generator reuse
simonrozsival May 6, 2026
075170b
[TrimmableTypeMap] Keep array rank anchors private
simonrozsival May 6, 2026
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 @@ -57,6 +57,7 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua
// This needs to be called first, since it sets up locations, environment variables, logging etc
XA_Host_NativeAOT_OnInit (language, filesDir, cacheDir, ref initArgs);
JNIEnvInit.InitializeJniRuntimeEarly (initArgs);
JNIEnvInit.InitializeNativeAotTrimmableTypeMapData ();

var settings = new DiagnosticSettings ();
settings.AddDebugDotnetLog ();
Expand All @@ -75,6 +76,7 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua

// Entry point into Mono.Android.dll. Log categories are initialized in JNI_OnLoad.
JNIEnvInit.InitializeJniRuntime (runtime, initArgs);
JNIEnvInit.RegisterNativeAotTrimmableTypeMapNativeMethods ();

transition = new JniTransition (jnienv);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ sealed class TypeMapAssemblyData
/// </summary>
public List<AliasHolderData> AliasHolders { get; } = new ();

/// <summary>
/// Maximum array rank for which the generator emits per-rank <c>__ArrayMapRank{N}</c>
/// sentinel TypeDefs and <c>TypeMap</c> entries. 0 disables.
/// </summary>
public int MaxArrayRank { get; set; }

/// <summary>
/// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls.
/// </summary>
Expand Down Expand Up @@ -77,6 +83,12 @@ sealed record TypeMapAttributeData
/// True for 2-arg unconditional entries (ACW types, essential runtime types).
/// </summary>
public bool IsUnconditional => TargetTypeReference == null;

/// <summary>
/// 1-based array rank when this entry should use a <c>__ArrayMapRank{value}</c>
/// sentinel as its <c>TGroup</c> instead of the default model anchor.
/// </summary>
public int? AnchorRank { get; init; }
}

/// <summary>
Expand Down
126 changes: 95 additions & 31 deletions src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ static class ModelBuilder
{
const string ProxyTypeSuffix = "_Proxy";

// Workaround for https://github.com/dotnet/runtime/issues/127004
// When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the
// trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute
// references the same type. Set to false once the runtime bug is fixed to re-enable
// 3-arg conditional entries that allow unused framework bindings to be trimmed away.
const bool ForceUnconditionalEntries = true;

static readonly HashSet<string> EssentialRuntimeTypes = new (StringComparer.Ordinal) {
"java/lang/Object",
"java/lang/Class",
Expand All @@ -40,21 +33,35 @@ static class ModelBuilder
/// <param name="peers">Scanned Java peer types (typically from a single input assembly).</param>
/// <param name="outputPath">Output .dll path — used to derive assembly/module names if not specified.</param>
/// <param name="assemblyName">Explicit assembly name. If null, derived from <paramref name="outputPath"/>.</param>
public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, string outputPath, string? assemblyName = null)
/// <param name="maxArrayRank">
/// Emit per-rank array <c>TypeMap</c> entries + <c>__ArrayMapRank{N}</c> sentinels
/// for ranks 1..<paramref name="maxArrayRank"/>. 0 disables array entry emission.
/// </param>
/// <param name="forceUnconditionalEntries">True to emit all TypeMap entries as unconditional 2-arg attributes.</param>
public static TypeMapAssemblyData Build (
IReadOnlyList<JavaPeerInfo> peers,
string outputPath,
string? assemblyName = null,
int maxArrayRank = 0,
bool forceUnconditionalEntries = true,
ISet<string>? frameworkAssemblyNames = null)
{
if (peers is null) {
throw new ArgumentNullException (nameof (peers));
}
if (outputPath is null) {
throw new ArgumentNullException (nameof (outputPath));
}
if (maxArrayRank < 0) {
throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0.");
}

assemblyName ??= Path.GetFileNameWithoutExtension (outputPath);
string moduleName = Path.GetFileName (outputPath);

var model = new TypeMapAssemblyData {
AssemblyName = assemblyName,
ModuleName = moduleName,
ModuleName = Path.GetFileName (outputPath),
MaxArrayRank = maxArrayRank,
};

// Invoker types are NOT emitted as separate proxies or TypeMap entries.
Expand Down Expand Up @@ -89,7 +96,11 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName));
}

EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries, frameworkAssemblyNames);

if (maxArrayRank > 0) {
EmitArrayEntries (model, jniName, peersForName, maxArrayRank);
}
}

// Compute IgnoresAccessChecksTo from cross-assembly references
Expand All @@ -114,7 +125,8 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
}

static void EmitPeers (TypeMapAssemblyData model, string jniName,
List<JavaPeerInfo> peersForName, string assemblyName, HashSet<string> usedProxyNames)
List<JavaPeerInfo> peersForName, string assemblyName, HashSet<string> usedProxyNames, bool forceUnconditionalEntries,
ISet<string>? frameworkAssemblyNames)
{
bool isAliasGroup = peersForName.Count > 1;

Expand All @@ -130,7 +142,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
model.ProxyTypes.Add (proxy);
}

var entry = BuildEntry (peer, proxy, assemblyName, jniName);
var entry = BuildEntry (peer, proxy, assemblyName, jniName, forceUnconditionalEntries, frameworkAssemblyNames);
model.Entries.Add (entry);

// Emit a TypeMapAssociation for every entry that has a proxy.
Expand Down Expand Up @@ -165,7 +177,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
model.ProxyTypes.Add (proxy);
}

model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName));
model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName, forceUnconditionalEntries, frameworkAssemblyNames));

// Link each alias type to the alias holder for trimming
model.Associations.Add (new TypeMapAssociationData {
Expand All @@ -178,14 +190,14 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
}

// Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations)
// When ForceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just
// When forceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just
// like BuildEntry does: dotnet/runtime#127004 strips the TypeMapAssociation that keeps the
// holder alive when a TypeMap entry references the same type, leaving the dictionary key
// missing at runtime and breaking hierarchy lookups for essential types like
// java/lang/String and java/lang/Object.
bool aliasBaseUnconditional = ForceUnconditionalEntries
bool aliasBaseUnconditional = forceUnconditionalEntries
|| EssentialRuntimeTypes.Contains (jniName)
|| peersForName.Any (IsUnconditionalEntry);
|| peersForName.Any (p => IsUnconditionalEntry (p, forceUnconditionalEntries: false, frameworkAssemblyNames));
model.Entries.Add (new TypeMapAttributeData {
JniName = jniName,
ProxyTypeReference = holderRef,
Expand Down Expand Up @@ -219,27 +231,32 @@ static void AddProxyAssociation (TypeMapAssemblyData model, string managedTypeNa
/// Determines whether a type should use the unconditional (2-arg) TypeMap attribute.
/// Unconditional types are always preserved by the trimmer.
/// </summary>
static bool IsUnconditionalEntry (JavaPeerInfo peer)
static bool IsUnconditionalEntry (JavaPeerInfo peer, bool forceUnconditionalEntries, ISet<string>? frameworkAssemblyNames)
{
// Essential runtime types needed by the Java interop runtime
if (forceUnconditionalEntries) {
return true;
}

if (EssentialRuntimeTypes.Contains (peer.JavaName)) {
return true;
}

// User-defined ACW types (not MCW bindings, not interfaces) are unconditional
// because Android can instantiate them from Java at any time.
if (!peer.DoNotGenerateAcw && !peer.IsInterface) {
if (!peer.DoNotGenerateAcw && !peer.IsInterface && !IsFrameworkAssembly (peer, frameworkAssemblyNames)) {
return true;
}

// Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
if (peer.IsUnconditional) {
return true;
}

return false;
}

static bool IsFrameworkAssembly (JavaPeerInfo peer, ISet<string>? frameworkAssemblyNames)
{
return frameworkAssemblyNames is not null && frameworkAssemblyNames.Contains (peer.AssemblyName);
}

static void AddIfCrossAssembly (SortedSet<string> set, string? asmName, string outputAssemblyName)
{
if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) {
Expand Down Expand Up @@ -384,7 +401,8 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy)
}

static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy,
string outputAssemblyName, string jniName)
string outputAssemblyName, string jniName, bool forceUnconditionalEntries,
ISet<string>? frameworkAssemblyNames)
{
string proxyRef;
if (proxy != null) {
Expand All @@ -393,13 +411,8 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr
proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
}

// When ForceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap
// attributes to work around https://github.com/dotnet/runtime/issues/127004.
bool isUnconditional = ForceUnconditionalEntries || IsUnconditionalEntry (peer);
string? targetRef = null;
if (!isUnconditional) {
targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);
}
bool isUnconditional = IsUnconditionalEntry (peer, forceUnconditionalEntries, frameworkAssemblyNames);
string? targetRef = isUnconditional ? null : AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName);

return new TypeMapAttributeData {
JniName = jniName,
Expand All @@ -410,4 +423,55 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr

static string AssemblyQualify (string typeName, string assemblyName)
=> $"{typeName}, {assemblyName}";

/// <summary>
/// Emits per-rank array TypeMap entries for one peer, anchored to the per-assembly
/// <c>__ArrayMapRank{N}</c> sentinels. Keys are bare element JNI names (rank is encoded
/// by the sentinel anchor, not by JNI array prefixes). Skips open generics, primitive JNI
/// keyword keys (handled by the legacy primitive-array path), and alias groups.
/// </summary>
static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List<JavaPeerInfo> peersForName, int maxArrayRank)
{
if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) {
return;
}
if (peersForName.Count != 1) {
return;
}

var peer = peersForName [0];
if (peer.IsGenericDefinition) {
return;
}

for (int rank = 1; rank <= maxArrayRank; rank++) {
string arrayTypeRef = AssemblyQualify (peer.ManagedTypeName + Brackets (rank), peer.AssemblyName);
model.Entries.Add (new TypeMapAttributeData {
JniName = jniName,
ProxyTypeReference = arrayTypeRef,
TargetTypeReference = arrayTypeRef,
AnchorRank = rank,
});
}
}

static string Brackets (int rank) => rank switch {
1 => "[]",
2 => "[][]",
3 => "[][][]",
_ => BuildBrackets (rank),
};

static string BuildBrackets (int rank)
{
var sb = new StringBuilder (rank * 2);
for (int i = 0; i < rank; i++) {
sb.Append ("[]");
}
return sb.ToString ();
}

static bool IsJniPrimitiveKeyword (char c)
=> c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I'
|| c == 'J' || c == 'F' || c == 'D' || c == 'V';
}
Loading