Skip to content

Commit 64f1a30

Browse files
[TrimmableTypeMap] Runtime trimmable typemap support (#11090)
* [TrimmableTypeMap] Runtime trimmable typemap support Rebuild the remaining runtime-only portion of PR 11090 on top of main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Keep cached proxy-only JNI lookup Remove the accidental fallback chain from TryGetJniName while preserving the cache. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Remove legacy TypeManager redirect Keep the trimmable peer path on TrimmableTypeMapTypeManager/JavaMarshalValueManager, make any legacy TypeManager lookup in trimmable mode fail fast, and tighten the proxy caches/state handling for parity with the existing activation flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Fail fast on missing peer proxies Stop silently falling back when trimmable peer activation cannot resolve a generated JavaPeerProxy. In trimmable mode, missing proxy coverage should surface as an explicit runtime error so generator gaps are fixed at the source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Remove duplicate JNI name helper Collapse the trimmable managed-to-JNI lookup back to a single internal API by keeping TryGetJniNameForManagedType() and removing the duplicate TryGetJniName() entry point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Prune internal peer helpers Reduce TrimmableTypeMap surface area by making purely local proxy helpers private and removing the unused TryCreatePeer() wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Clarify Java object proxy lookup Rename GetProxyForPeer() to GetProxyForJavaObject() so the helper more clearly describes what it actually inspects and resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TrimmableTypeMap] Restore CreatePeer guard semantics Preserve the inherited JniValueManager.CreatePeer() preconditions for disposed managers and invalid JNI references before the trimmable fail-fast proxy check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix trimmable typemap review issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refine trimmable typemap cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Inline sentinel cache handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reuse managed proxy lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename typemap target lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extract Java proxy cache helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Separate proxy lookup from activation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Refactor Java proxy resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Inline typemap cache resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tidy typemap helper flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fail fast on typemap aliases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Runtime helper qualification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify Java proxy target lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 36e7c08 commit 64f1a30

6 files changed

Lines changed: 145 additions & 45 deletions

File tree

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,12 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr)
442442

443443
internal static unsafe string? TypemapManagedToJava (Type type)
444444
{
445+
if (RuntimeFeature.TrimmableTypeMap) {
446+
// The trimmable typemap doesn't use the native typemap tables.
447+
// Delegate to the managed TrimmableTypeMap instead.
448+
return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null;
449+
}
450+
445451
if (mvid_bytes == null)
446452
mvid_bytes = new byte[16];
447453

src/Mono.Android/Android.Runtime/JNIEnvInit.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args)
110110
internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args)
111111
{
112112
androidRuntime = runtime;
113+
JniRuntime.SetCurrent (runtime);
113114
SetSynchronizationContext ();
114115
}
115116

@@ -159,6 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
159160
valueManager,
160161
args->jniAddNativeMethodRegistrationAttributePresent != 0
161162
);
163+
JniRuntime.SetCurrent (androidRuntime);
162164

163165
if (RuntimeFeature.TrimmableTypeMap) {
164166
TrimmableTypeMap.Initialize ();

src/Mono.Android/Java.Interop/TypeManager.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,12 @@ static Type monovm_typemap_java_to_managed (string java_type_name)
268268
return type;
269269
}
270270

271-
if (RuntimeFeature.IsMonoRuntime) {
271+
if (RuntimeFeature.TrimmableTypeMap) {
272+
throw new System.Diagnostics.UnreachableException (
273+
$"{nameof (TypeManager)}.{nameof (GetJavaToManagedTypeCore)} should not be used when " +
274+
$"{nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. The trimmable path should resolve " +
275+
$"types through {nameof (TrimmableTypeMapTypeManager)}.");
276+
} else if (RuntimeFeature.IsMonoRuntime) {
272277
type = monovm_typemap_java_to_managed (class_name);
273278
} else if (RuntimeFeature.IsCoreClrRuntime) {
274279
type = clr_typemap_java_to_managed (class_name);

src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -507,18 +507,35 @@ void ProcessContext (HandleContext* context)
507507
[DynamicallyAccessedMembers (Constructors)]
508508
Type? targetType)
509509
{
510+
ThrowIfDisposed ();
511+
512+
if (!reference.IsValid) {
513+
return null;
514+
}
515+
510516
if (RuntimeFeature.TrimmableTypeMap) {
511-
var typeMap = TrimmableTypeMap.Instance;
512-
if (typeMap is not null && targetType is not null) {
513-
var proxy = typeMap.GetProxyForManagedType (targetType);
514-
if (proxy is not null) {
515-
var peer = proxy.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
516-
if (peer is not null) {
517-
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
518-
JniObjectReference.Dispose (ref reference, transfer);
519-
return peer;
517+
try {
518+
var typeMap = TrimmableTypeMap.Instance;
519+
var proxy = typeMap.GetProxyForJavaObject (reference.Handle, targetType);
520+
var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
521+
if (peer is not null) {
522+
var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable;
523+
if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) {
524+
peerState |= JniManagedPeerStates.Activatable;
520525
}
526+
peer.SetJniManagedPeerState (peerState);
527+
return peer;
521528
}
529+
530+
var targetName = targetType?.AssemblyQualifiedName ?? "<null>";
531+
var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference);
532+
533+
throw new NotSupportedException (
534+
$"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " +
535+
$"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " +
536+
$"This indicates a missing trimmable typemap proxy or association and should be fixed in the generator.");
537+
} finally {
538+
JniObjectReference.Dispose (ref reference, transfer);
522539
}
523540
}
524541

src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs

Lines changed: 104 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace Microsoft.Android.Runtime;
2020
class TrimmableTypeMap
2121
{
2222
static readonly Lock s_initLock = new ();
23+
static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy ();
2324
static TrimmableTypeMap? s_instance;
2425

2526
internal static TrimmableTypeMap Instance =>
@@ -28,7 +29,8 @@ class TrimmableTypeMap
2829

2930
readonly IReadOnlyDictionary<string, Type> _typeMap;
3031
readonly IReadOnlyDictionary<Type, Type> _proxyTypeMap;
31-
readonly ConcurrentDictionary<Type, JavaPeerProxy?> _proxyCache = new ();
32+
readonly ConcurrentDictionary<Type, JavaPeerProxy> _proxyCache = new ();
33+
readonly ConcurrentDictionary<string, JavaPeerProxy> _peerProxyCache = new (StringComparer.Ordinal);
3234

3335
TrimmableTypeMap ()
3436
{
@@ -55,9 +57,6 @@ internal static void Initialize ()
5557
}
5658
}
5759

58-
/// <summary>
59-
/// Registers the <c>mono.android.Runtime.registerNatives</c> JNI native method.
60-
/// </summary>
6160
unsafe void RegisterNatives ()
6261
{
6362
using var runtimeClass = new JniType ("mono/android/Runtime"u8);
@@ -68,49 +67,111 @@ unsafe void RegisterNatives ()
6867
}
6968
}
7069

71-
internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type)
72-
=> _typeMap.TryGetValue (jniSimpleReference, out type);
70+
internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type)
71+
{
72+
type = GetProxyForJavaType (jniSimpleReference)?.TargetType;
73+
return type is not null;
74+
}
7375

74-
/// <summary>
75-
/// Finds the proxy for a managed type using the generated proxy type map.
76-
/// Results are cached per type.
77-
/// </summary>
78-
internal JavaPeerProxy? GetProxyForManagedType (Type managedType)
79-
=> _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type), this);
76+
JavaPeerProxy? GetProxyForManagedType (Type managedType)
77+
{
78+
var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => {
79+
if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) {
80+
return s_noPeerSentinel;
81+
}
82+
83+
return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false) ?? s_noPeerSentinel;
84+
}, this);
85+
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
86+
}
8087

81-
JavaPeerProxy? ResolveProxyForManagedType (Type managedType)
88+
JavaPeerProxy? GetProxyForJavaType (string className)
8289
{
83-
if (!_proxyTypeMap.TryGetValue (managedType, out var proxyType)) {
84-
return null;
85-
}
90+
var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => {
91+
if (!self._typeMap.TryGetValue (name, out var mappedType)) {
92+
return s_noPeerSentinel;
93+
}
8694

87-
return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false);
95+
var proxy = mappedType.GetCustomAttribute<JavaPeerProxy> (inherit: false);
96+
if (proxy is null) {
97+
// Alias typemap entries (for example "jni/name[1]") are not implemented yet.
98+
// Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788.
99+
throw new NotImplementedException (
100+
$"Trimmable typemap alias handling is not implemented yet for '{name}'.");
101+
}
102+
103+
return proxy;
104+
}, this);
105+
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
88106
}
89107

90108
internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName)
91109
{
92-
var proxy = GetProxyForManagedType (managedType);
93-
if (proxy is not null) {
94-
jniName = proxy.JniName;
95-
return true;
96-
}
97-
98-
jniName = null;
99-
return false;
110+
jniName = GetProxyForManagedType (managedType)?.JniName;
111+
return jniName is not null;
100112
}
101113

102-
/// <summary>
103-
/// Creates a peer instance using the proxy's CreateInstance method.
104-
/// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance.
105-
/// </summary>
106-
internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer)
114+
internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null)
107115
{
108-
var proxy = GetProxyForManagedType (type);
109-
if (proxy is null) {
110-
return false;
116+
if (handle == IntPtr.Zero) {
117+
return null;
111118
}
112119

113-
return proxy.CreateInstance (handle, transfer) != null;
120+
return TryGetProxyFromHierarchy (this, handle, targetType) ??
121+
TryGetProxyFromTargetType (this, handle, targetType);
122+
123+
static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType)
124+
{
125+
var selfRef = new JniObjectReference (handle);
126+
var jniClass = JniEnvironment.Types.GetObjectClass (selfRef);
127+
128+
try {
129+
while (jniClass.IsValid) {
130+
var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass);
131+
if (className != null) {
132+
var proxy = self.GetProxyForJavaType (className);
133+
if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) {
134+
return proxy;
135+
}
136+
}
137+
138+
var super = JniEnvironment.Types.GetSuperclass (jniClass);
139+
JniObjectReference.Dispose (ref jniClass);
140+
jniClass = super;
141+
}
142+
} finally {
143+
JniObjectReference.Dispose (ref jniClass);
144+
}
145+
146+
return null;
147+
}
148+
149+
static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType)
150+
{
151+
if (targetType is null) {
152+
return null;
153+
}
154+
155+
var proxy = self.GetProxyForManagedType (targetType);
156+
// Verify the Java object is actually assignable to the target Java type
157+
// before returning the fallback proxy. Without this, we'd create invalid peers
158+
// (e.g., IAppendableInvoker wrapping a java.lang.Integer).
159+
if (proxy is null || !self.TryGetJniNameForManagedType (targetType, out var targetJniName)) {
160+
return null;
161+
}
162+
163+
var selfRef = new JniObjectReference (handle);
164+
var objClass = default (JniObjectReference);
165+
var targetClass = default (JniObjectReference);
166+
try {
167+
objClass = JniEnvironment.Types.GetObjectClass (selfRef);
168+
targetClass = JniEnvironment.Types.FindClass (targetJniName);
169+
return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null;
170+
} finally {
171+
JniObjectReference.Dispose (ref objClass);
172+
JniObjectReference.Dispose (ref targetClass);
173+
}
174+
}
114175
}
115176

116177
const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
@@ -162,4 +223,13 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa
162223
}
163224
}
164225

226+
sealed class MissingJavaPeerProxy : JavaPeerProxy
227+
{
228+
public MissingJavaPeerProxy () : base ("<missing>", typeof (Java.Lang.Object), null)
229+
{
230+
}
231+
232+
public override IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) => null;
233+
}
234+
165235
}

src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpl
2121
yield return t;
2222
}
2323

24-
if (TrimmableTypeMap.Instance.TryGetType (jniSimpleReference, out var type)) {
24+
if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) {
2525
yield return type;
2626
}
2727
}

0 commit comments

Comments
 (0)