Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6f39de4
[NativeAOT] Initialize trimmable typemap runtime
simonrozsival May 5, 2026
50912dd
[NativeAOT] Use conditional trimmable typemap entries
simonrozsival May 5, 2026
0778e69
[NativeAOT] Avoid exporting framework typemap assemblies
simonrozsival May 5, 2026
7e539cc
[NativeAOT] Simplify trimmable typemap configuration
simonrozsival May 5, 2026
da30f7e
[NativeAOT] Handle GC peer class lookup failures
simonrozsival May 5, 2026
4546fd9
[TrimmableTypeMap] Treat framework ACWs as conditional
simonrozsival May 5, 2026
a393bd4
[TrimmableTypeMap] Remove inclusion decision diagnostics
simonrozsival May 6, 2026
b726b83
[TrimmableTypeMap] Move force-unconditional flag to generator state
simonrozsival May 6, 2026
cb6351a
[NativeAOT] Split CoreCLR and NativeAOT runtime branches
simonrozsival May 6, 2026
a73e94c
[TrimmableTypeMap] Remove assembly summary logging
simonrozsival May 6, 2026
2aee294
[TrimmableTypeMap] Use type map assembly generator wrapper
simonrozsival May 6, 2026
1b24cd7
[TrimmableTypeMap] Simplify assembly generator reuse
simonrozsival May 6, 2026
4ad7b46
[NativeAOT] Consolidate JNI runtime initialization
simonrozsival May 6, 2026
a8ddd77
[NativeAOT] Make array typemap anchors public
simonrozsival May 12, 2026
303b6c3
Fix NativeAOT linker errors for __ArrayMapRank vtables
simonrozsival May 12, 2026
e0429a7
Use 2-arg Initialize overload when maxArrayRank is 0
simonrozsival May 13, 2026
61403a5
Merge main and fix TrackedInstructionEncoder API usage
simonrozsival May 13, 2026
e96a106
Update MonoVM apkdesc baselines
simonrozsival May 13, 2026
a192c99
Set IsNativeAotRuntime=false for MonoVM and CoreCLR builds
simonrozsival May 13, 2026
f48da6d
Reset Java.Interop submodule to main
simonrozsival May 13, 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 @@ -56,25 +56,23 @@ 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.InitializeBeforeRuntimeCreation (initArgs);

var settings = new DiagnosticSettings ();
settings.AddDebugDotnetLog ();

var typeManager = CreateTypeManager ();

var options = new NativeAotRuntimeOptions {
EnvironmentPointer = jnienv,
ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global),
TypeManager = typeManager,
ValueManager = new JavaMarshalValueManager (),
TypeManager = JNIEnvInit.CreateTypeManager (initArgs),
ValueManager = JNIEnvInit.CreateValueManager (),
JniGlobalReferenceLogWriter = settings.GrefLog,
JniLocalReferenceLogWriter = settings.LrefLog,
};
runtime = options.CreateJreVM ();

// Entry point into Mono.Android.dll. Log categories are initialized in JNI_OnLoad.
JNIEnvInit.InitializeJniRuntime (runtime, initArgs);
// Entry point into Mono.Android.dll for NativeAOT-specific JNI runtime initialization.
JNIEnvInit.InitializeNativeAotRuntime (runtime, initArgs);

transition = new JniTransition (jnienv);

Expand All @@ -87,13 +85,4 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua
}
transition.Dispose ();
}

static JniRuntime.JniTypeManager CreateTypeManager ()
{
if (RuntimeFeature.TrimmableTypeMap) {
return new TrimmableTypeMapTypeManager ();
}

return new ManagedTypeManager ();
}
}
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 @@ -44,7 +37,14 @@ static class ModelBuilder
/// 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>
public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0)
/// <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));
Expand Down Expand Up @@ -96,7 +96,7 @@ 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);
Expand Down Expand Up @@ -125,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 @@ -141,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 @@ -176,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 @@ -189,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 @@ -230,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 @@ -397,7 +403,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 @@ -406,13 +413,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,25 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand
var externalDictArrayTypeSpec = MakeIReadOnlyDictArrayTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true);

if (useSharedTypemapUniverse) {
var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef,
initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank);
if (maxArrayRank > 0) {
var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef,
initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank);
} else {
var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef);
}
} else {
var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false);
EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef,
initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank);
if (maxArrayRank > 0) {
var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef,
initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank);
} else {
var initializeRef = AddInitializeAggregateNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef);
EmitInitializeWithAggregateTypeMapNoArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef,
initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef);
}
}
}

Expand Down Expand Up @@ -325,6 +336,70 @@ static void EmitFillArrayLocal (TrackedInstructionEncoder encoder, int count, En
}
}

/// <summary>
/// Aggregate IL emit without array maps. Calls the 2-arg overload:
/// <c>TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[])</c>.
/// </summary>
static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe,
IReadOnlyList<string> perAssemblyTypeMapNames,
MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef,
MemberReferenceHandle initializeRef,
TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec,
TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef)
{
var count = perAssemblyTypeMapNames.Count;

var getExternalSpecs = new EntityHandle [count];
var getProxySpecs = new EntityHandle [count];
for (int i = 0; i < count; i++) {
var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]);
var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef,
default, pe.Metadata.GetOrAddString ("__TypeMapAnchor"));
getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef);
getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef);
}

pe.EmitBody ("Initialize",
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }),
encoder => {
EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0);
EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0);

EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1);
EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1);

encoder.LoadLocal (0);
encoder.LoadLocal (1);
encoder.Call (initializeRef, parameterCount: 2);
encoder.Return ();
},
encodeLocals: localsSig => {
localsSig.WriteByte (0x07); // LOCAL_SIG
localsSig.WriteCompressedInteger (2);
localsSig.WriteByte (0x1D);
EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true);
localsSig.WriteByte (0x1D);
EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false);
});
}

/// <summary>MemberRef for <c>TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[])</c> (2-arg, no array maps).</summary>
static MemberReferenceHandle AddInitializeAggregateNoArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef,
TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef)
{
var blob = new BlobBuilder (64);
blob.WriteByte (0x00); // DEFAULT (static)
blob.WriteCompressedInteger (2); // parameter count
blob.WriteByte (0x01); // return type: void
blob.WriteByte (0x1D);
EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true);
blob.WriteByte (0x1D);
EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false);
return pe.Metadata.AddMemberReference (trimmableTypeMapRef,
pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob));
}

/// <summary>MemberRef for <c>TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], arrayMapsByAssemblyAndRank[][])</c>.</summary>
static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef,
TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef)
Expand Down Expand Up @@ -375,6 +450,42 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle
});
}

/// <summary>
/// Shared-universe IL emit without array maps. Calls the simpler 2-arg overload:
/// <c>TrimmableTypeMap.Initialize(typeMap, proxyMap)</c>.
/// </summary>
static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle,
MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef,
MemberReferenceHandle initializeRef)
{
var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle);
var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle);

pe.EmitBody ("Initialize",
MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig,
sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }),
encoder => {
encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true);
encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true);
encoder.Call (initializeRef, parameterCount: 2);
encoder.Return ();
});
}

/// <summary>MemberRef for <c>TrimmableTypeMap.Initialize(typeMap, proxyMap)</c> (2-arg, no array maps).</summary>
static MemberReferenceHandle AddInitializeSingleNoArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef,
TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef)
{
var blob = new BlobBuilder (64);
blob.WriteByte (0x00); // DEFAULT (static)
blob.WriteCompressedInteger (2); // parameter count
blob.WriteByte (0x01); // return type: void
EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true);
EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false);
return pe.Metadata.AddMemberReference (trimmableTypeMapRef,
pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob));
}

/// <summary>MemberRef for <c>TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByAssemblyAndRank[][])</c>.</summary>
static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef,
TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,11 @@ void EmitAnchorType ()
}

/// <summary>
/// Emits private <c>__ArrayMapRank{N}</c> classes used as group type parameters
/// Emits public <c>__ArrayMapRank{N}</c> classes used as group type parameters
/// for array <c>TypeMap&lt;T&gt;</c> entries. Each per-assembly typemap owns its
/// rank anchors so array maps stay scoped to the generated assembly.
/// rank anchors so array maps stay scoped to the generated assembly. The root
/// typemap assembly references these anchors, so NativeAOT requires them to be
/// visible across assembly boundaries.
/// </summary>
void EmitRankSentinels (TypeMapAssemblyData model)
{
Expand All @@ -321,7 +323,7 @@ void EmitRankSentinels (TypeMapAssemblyData model)
_pe.Metadata.GetOrAddString ("System"), _pe.Metadata.GetOrAddString ("Object"));
for (int i = 0; i < model.MaxArrayRank; i++) {
_rankAnchorHandles [i] = _pe.Metadata.AddTypeDefinition (
TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class,
TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class,
default,
_pe.Metadata.GetOrAddString ($"__ArrayMapRank{i + 1}"),
objectRef,
Expand Down
Loading