Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8e83f3f
Fix trimmable typemap startup ordering
simonrozsival Apr 30, 2026
1f7fd6f
Fix root typemap target anchors
simonrozsival Apr 30, 2026
e62ae8b
Handle non-generated JNI peers without proxies
simonrozsival Apr 30, 2026
3ee7d95
Split typemap target attribute emitters
simonrozsival Apr 30, 2026
555de15
Ignore JniTypeSignature in trimmable typemaps
simonrozsival Apr 30, 2026
99a15ac
Address trimmable typemap review feedback
simonrozsival Apr 30, 2026
2a44d9c
Add registered peer trimmable typemap coverage
simonrozsival Apr 30, 2026
c0c1a01
Fix CoreCLR trimmable linker root
simonrozsival Apr 30, 2026
4be3906
Apply CoreCLR exclusions to trimmable typemap tests
simonrozsival Apr 30, 2026
92675c8
Prepare trimmable typemap assemblies for packaging
simonrozsival May 1, 2026
d787990
Preserve trimmable component metadata
simonrozsival May 1, 2026
2321cbd
Associate invoker types with typemap proxies
simonrozsival May 1, 2026
92d7f13
Resolve JNI names from trimmable typemaps
simonrozsival May 1, 2026
55ad557
Emit raw trimmable UCO registrations
simonrozsival May 1, 2026
cbe9d93
Merge origin/main into trimmable typemap startup fixes
simonrozsival May 1, 2026
4356d3e
Complete trimmable typemap validation fixes
simonrozsival May 1, 2026
f66dbb6
Use utf-8 overload of JniType ctor
simonrozsival May 1, 2026
2bf7294
Compute typemap IL maxstack
simonrozsival May 1, 2026
5167e24
Move maxstack work to follow-up
simonrozsival May 1, 2026
bec48b8
Simplify trimmable typemap test code
simonrozsival May 1, 2026
d108b9c
Remove brittle typemap IL token assertions
simonrozsival May 1, 2026
7a7618f
Preserve startup hook in runtime tests
simonrozsival May 1, 2026
0bf8b07
Fix CoreCLR debug typemap duplicates
simonrozsival May 1, 2026
baf509c
Remove broad trimmable test roots
simonrozsival May 1, 2026
1295da7
Remove transitive reference suppression
simonrozsival May 1, 2026
5598f96
Remove trimmable test root validation target
simonrozsival May 2, 2026
6327757
Avoid standalone Java.Interop in runtime tests
simonrozsival May 2, 2026
54590fe
Merge origin/main into trimmable-typemap-startup-fixes
simonrozsival May 2, 2026
762b83f
Rename type map lookup to GetProxyTypes
simonrozsival May 4, 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 @@ -120,11 +120,10 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
if (!isAliasGroup) {
// Single peer — no aliases needed, emit directly with the base JNI name
var peer = peersForName [0];
bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;

JavaPeerProxyData? proxy = null;
if (hasProxy) {
if (NeedsProxy (peer)) {
proxy = BuildProxyType (peer, jniName, usedProxyNames, isAcw);
model.ProxyTypes.Add (proxy);
}
Expand Down Expand Up @@ -158,11 +157,10 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
string entryJniName = $"{jniName}[{i}]";
aliasKeys.Add (entryJniName);

bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;

JavaPeerProxyData? proxy = null;
if (hasProxy) {
if (NeedsProxy (peer)) {
proxy = BuildProxyType (peer, jniName, usedProxyNames, isAcw);
model.ProxyTypes.Add (proxy);
}
Expand Down Expand Up @@ -223,6 +221,11 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer)
return false;
}

static bool NeedsProxy (JavaPeerInfo peer)
{
return peer.ActivationCtor != null || peer.InvokerTypeName != null;
}

static void AddIfCrossAssembly (SortedSet<string> set, string? asmName, string outputAssemblyName)
{
if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ public void Generate (IReadOnlyList<string> perAssemblyTypeMapNames, bool useSha
MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1));
}

// Emit [assembly: TypeMapAssemblyTargetAttribute<__TypeMapAnchor>("name")] for each per-assembly typemap
EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames);
// Emit [assembly: TypeMapAssemblyTargetAttribute<T>("name")] for each per-assembly typemap.
// T must match the group type later passed to TypeMapping.GetOrCreate*TypeMapping<T>().
EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse);

// Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access
// internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android,
Expand All @@ -126,20 +127,31 @@ public void Generate (IReadOnlyList<string> perAssemblyTypeMapNames, bool useSha
pe.WritePE (stream);
}

static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList<string> perAssemblyTypeMapNames)
static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList<string> perAssemblyTypeMapNames, bool useSharedTypemapUniverse)
{
var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef,
pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"),
pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1"));

var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, anchorTypeHandle);
MemberReferenceHandle GetCtorRef (EntityHandle targetAnchorType)
{
var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, targetAnchorType);
return pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
rt => rt.Void (),
p => p.AddParameter ().Type ().String ()));
}

var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor",
sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1,
rt => rt.Void (),
p => p.AddParameter ().Type ().String ()));
var sharedCtorRef = useSharedTypemapUniverse ? GetCtorRef (anchorTypeHandle) : default;

foreach (var name in perAssemblyTypeMapNames) {
var ctorRef = sharedCtorRef;
if (!useSharedTypemapUniverse) {
var asmRef = pe.FindOrAddAssemblyRef (name);
var perAssemblyAnchorRef = pe.Metadata.AddTypeReference (asmRef,
default, pe.Metadata.GetOrAddString ("__TypeMapAnchor"));
ctorRef = GetCtorRef (perAssemblyAnchorRef);
}
var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name));
pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
var (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface);

// Resolve activation constructor
var activationCtor = ResolveActivationCtor (fullName, typeDef, index);
var activationCtor = ResolveActivationCtor (fullName, typeDef, index, registerInfo);

// For interfaces/abstract types, try to find invoker type name
if (isInterface || isAbstract) {
Expand Down Expand Up @@ -1129,7 +1129,7 @@ string ManagedTypeToJniDescriptor (string managedType)
};
}

ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index)
ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index, RegisterInfo? registerInfo)
{
var cacheKey = (typeName, index.AssemblyName);
if (activationCtorCache.TryGetValue (cacheKey, out var cached)) {
Expand All @@ -1144,13 +1144,18 @@ string ManagedTypeToJniDescriptor (string managedType)
return info;
}

if (ShouldSuppressInheritedActivationCtor (registerInfo)) {
return null;
}

// Walk base type hierarchy
var baseInfo = GetBaseTypeInfo (typeDef, index);
if (baseInfo is not null) {
var (baseTypeName, baseAssemblyName) = baseInfo.Value;
if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) {
var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex);
baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegisterInfo);
var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex, baseRegisterInfo);
if (result is not null) {
activationCtorCache [cacheKey] = result;
}
Expand All @@ -1161,6 +1166,11 @@ string ManagedTypeToJniDescriptor (string managedType)
return null;
}

static bool ShouldSuppressInheritedActivationCtor (RegisterInfo? registerInfo)
{
return registerInfo is { IsFromJniTypeSignature: true, DoNotGenerateAcw: true };
}

static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index)
{
foreach (var methodHandle in typeDef.GetMethods ()) {
Expand Down
14 changes: 10 additions & 4 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
java_class_loader = args->grefLoader;

BoundExceptionType = (BoundExceptionType)args->ioExceptionType;
if (RuntimeFeature.TrimmableTypeMap) {
InitializeTrimmableTypeMapData ();
}

JniRuntime.JniTypeManager typeManager;
JniRuntime.JniValueManager? valueManager = null;
if (RuntimeFeature.TrimmableTypeMap) {
Expand Down Expand Up @@ -161,6 +165,11 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
args->jniAddNativeMethodRegistrationAttributePresent != 0
);
JniRuntime.SetCurrent (androidRuntime);
if (RuntimeFeature.TrimmableTypeMap) {
// TypeMapLoader.Initialize() only loads managed typemap data. Registering
// mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader.
TrimmableTypeMap.RegisterNativeMethods ();
}

grefIGCUserPeer_class = args->grefIGCUserPeer;
grefGCUserPeerable_class = args->grefGCUserPeerable;
Expand All @@ -179,9 +188,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
if (!RuntimeFeature.TrimmableTypeMap) {
args->registerJniNativesFn = (IntPtr)(delegate* unmanaged<IntPtr, int, IntPtr, IntPtr, int, void>)&RegisterJniNatives;
}
if (RuntimeFeature.TrimmableTypeMap) {
InitializeTrimmableTypeMap ();
}
RunStartupHooksIfNeeded ();
SetSynchronizationContext ();
}
Expand All @@ -193,7 +199,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
// Separate method so the JIT doesn't try to resolve TypeMapLoader (from _Microsoft.Android.TypeMaps.dll)
// when compiling JNIEnvInit.Initialize() in non-trimmable builds where that assembly isn't present.
[MethodImpl (MethodImplOptions.NoInlining)]
static void InitializeTrimmableTypeMap ()
static void InitializeTrimmableTypeMapData ()
{
TypeMapLoader.Initialize ();
}
Expand Down
13 changes: 11 additions & 2 deletions src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,19 @@ public AggregateTypeMap (SingleUniverseTypeMap[] universes)
_universes = universes;
}

public IEnumerable<Type> GetTypes (string jniName)
public IEnumerable<Type> GetTargetTypes (string jniName)
{
foreach (var universe in _universes) {
foreach (var type in universe.GetTypes (jniName)) {
foreach (var type in universe.GetTargetTypes (jniName)) {
yield return type;
}
}
}

public IEnumerable<Type> GetProxyTypes (string jniName)
{
foreach (var universe in _universes) {
foreach (var type in universe.GetProxyTypes (jniName)) {
yield return type;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ namespace Microsoft.Android.Runtime;
interface ITypeMapWithAliasing
{
/// <summary>
/// Returns all types mapped to a JNI name, resolving alias holders.
/// For non-alias entries this yields a single type. For alias groups
/// it follows each alias key and yields the surviving target types.
/// Returns all managed target types mapped to a JNI name, resolving alias holders.
/// Entries backed by generated proxy types return the proxy's target type.
/// </summary>
IEnumerable<Type> GetTypes (string jniName);
IEnumerable<Type> GetTargetTypes (string jniName);

/// <summary>
/// Returns generated proxy types mapped to a JNI name, resolving alias holders.
/// Entries without generated proxies are ignored.
/// </summary>
IEnumerable<Type> GetProxyTypes (string jniName);

/// <summary>
/// Resolves a managed type to its proxy type (the generated type that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,37 @@ public SingleUniverseTypeMap (IReadOnlyDictionary<string, Type> typeMap, IReadOn
_proxyTypeMap = proxyTypeMap;
}

public IEnumerable<Type> GetTypes (string jniName)
public IEnumerable<Type> GetTargetTypes (string jniName)
{
if (!_typeMap.TryGetValue (jniName, out var mappedType)) {
yield break;
foreach (var type in GetEntryTypes (jniName)) {
var proxy = type.GetCustomAttribute<JavaPeerProxy> (inherit: false);
yield return proxy?.TargetType ?? type;
}
}

// Fast path: non-alias entry
if (mappedType.GetCustomAttribute<JavaPeerProxy> (inherit: false) is not null) {
yield return mappedType;
public IEnumerable<Type> GetProxyTypes (string jniName)
{
foreach (var type in GetEntryTypes (jniName)) {
if (type.GetCustomAttribute<JavaPeerProxy> (inherit: false) is not null) {
yield return type;
}
}
}

IEnumerable<Type> GetEntryTypes (string jniName)
{
if (!_typeMap.TryGetValue (jniName, out var mappedType)) {
yield break;
}

// Slow path: alias holder — follow each alias key
var aliases = mappedType.GetCustomAttribute<JavaPeerAliasesAttribute> (inherit: false);
if (aliases is null) {
yield return mappedType;
yield break;
}

foreach (var key in aliases.Aliases) {
if (_typeMap.TryGetValue (key, out var aliasEntryType) &&
aliasEntryType.GetCustomAttribute<JavaPeerProxy> (inherit: false) is not null) {
if (_typeMap.TryGetValue (key, out var aliasEntryType)) {
yield return aliasEntryType;
}
}
Expand Down
41 changes: 30 additions & 11 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public class TrimmableTypeMap

readonly ITypeMapWithAliasing _typeMap;
readonly ConcurrentDictionary<Type, JavaPeerProxy> _proxyCache = new ();
readonly ConcurrentDictionary<string, Type[]> _jniTargetTypeCache = new (StringComparer.Ordinal);
readonly ConcurrentDictionary<string, JavaPeerProxy[]> _jniProxyCache = new (StringComparer.Ordinal);
bool _nativeMethodsRegistered;

TrimmableTypeMap (ITypeMapWithAliasing typeMap)
{
Expand Down Expand Up @@ -77,14 +79,23 @@ static void InitializeCore (ITypeMapWithAliasing typeMap)
throw new InvalidOperationException ("TrimmableTypeMap has already been initialized.");
}

var instance = new TrimmableTypeMap (typeMap);
instance.RegisterNatives ();
s_instance = instance;
s_instance = new TrimmableTypeMap (typeMap);
}
}

unsafe void RegisterNatives ()
internal static void RegisterNativeMethods ()
{
lock (s_initLock) {
Instance.RegisterNativeMethodsCore ();
}
}

unsafe void RegisterNativeMethodsCore ()
{
if (_nativeMethodsRegistered) {
throw new InvalidOperationException ("TrimmableTypeMap native methods have already been registered.");
}

// Use the `string` overload of `JniType` deliberately. Its underlying
// `JniEnvironment.Types.TryFindClass(string, bool)` tries raw JNI `FindClass`
// first and, if that fails, falls back to `Class.forName(name, true, info.Runtime.ClassLoader)`,
Expand All @@ -100,6 +111,7 @@ unsafe void RegisterNatives ()
var method = new JniNativeMethod (name, sig, onRegisterNatives);
JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]);
}
_nativeMethodsRegistered = true;
}

/// <summary>
Expand All @@ -109,19 +121,26 @@ unsafe void RegisterNatives ()
/// </summary>
internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[]? types)
{
var proxies = GetProxiesForJniName (jniName);
if (proxies.Length == 0) {
types = GetTargetTypesForJniName (jniName);
if (types.Length == 0) {
types = null;
return false;
}

types = new Type [proxies.Length];
for (int i = 0; i < proxies.Length; i++) {
types [i] = proxies [i].TargetType;
}
return true;
}

Type[] GetTargetTypesForJniName (string jniName)
{
return _jniTargetTypeCache.GetOrAdd (jniName, static (name, self) => {
var result = new List<Type> ();
foreach (var type in self._typeMap.GetTargetTypes (name)) {
result.Add (type);
}
return result.Count > 0 ? result.ToArray () : [];
}, this);
}

/// <summary>
/// Resolves and caches all proxies for a JNI name. For non-alias entries, returns a
/// single-element array. For alias groups, resolves each alias key and returns the
Expand All @@ -131,7 +150,7 @@ JavaPeerProxy[] GetProxiesForJniName (string jniName)
{
return _jniProxyCache.GetOrAdd (jniName, static (name, self) => {
var result = new List<JavaPeerProxy> ();
foreach (var type in self._typeMap.GetTypes (name)) {
foreach (var type in self._typeMap.GetProxyTypes (name)) {
var proxy = type.GetCustomAttribute<JavaPeerProxy> (inherit: false);
if (proxy is not null) {
result.Add (proxy);
Expand Down
Loading