Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3a8d509
[TrimmableTypeMap] Runtime trimmable typemap support
simonrozsival Apr 10, 2026
d4152df
[TrimmableTypeMap] Keep cached proxy-only JNI lookup
simonrozsival Apr 10, 2026
5d16a12
[TrimmableTypeMap] Remove legacy TypeManager redirect
simonrozsival Apr 10, 2026
656ddcf
[TrimmableTypeMap] Fail fast on missing peer proxies
simonrozsival Apr 10, 2026
5a96a71
[TrimmableTypeMap] Remove duplicate JNI name helper
simonrozsival Apr 10, 2026
aa33e56
[TrimmableTypeMap] Prune internal peer helpers
simonrozsival Apr 10, 2026
47089ad
[TrimmableTypeMap] Clarify Java object proxy lookup
simonrozsival Apr 10, 2026
c4b567c
[TrimmableTypeMap] Restore CreatePeer guard semantics
simonrozsival Apr 10, 2026
9c5672b
Fix trimmable typemap review issues
simonrozsival Apr 10, 2026
4a68393
Refine trimmable typemap cleanup
simonrozsival Apr 10, 2026
2897a08
Inline sentinel cache handling
simonrozsival Apr 10, 2026
7a7c186
Reuse managed proxy lookup
simonrozsival Apr 10, 2026
48102b6
Rename typemap target lookup
simonrozsival Apr 10, 2026
530b2b2
Extract Java proxy cache helper
simonrozsival Apr 10, 2026
0bad300
Separate proxy lookup from activation
simonrozsival Apr 10, 2026
f626700
Refactor Java proxy resolution
simonrozsival Apr 10, 2026
79ddbfb
Inline typemap cache resolution
simonrozsival Apr 10, 2026
cb32b91
Tidy typemap helper flow
simonrozsival Apr 10, 2026
af4a2df
Fail fast on typemap aliases
simonrozsival Apr 10, 2026
6df784c
Fix Runtime helper qualification
simonrozsival Apr 10, 2026
b9f3b1b
Simplify Java proxy target lookup
simonrozsival Apr 10, 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
6 changes: 6 additions & 0 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,12 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr)

internal static unsafe string? TypemapManagedToJava (Type type)
{
if (RuntimeFeature.TrimmableTypeMap) {
// The trimmable typemap doesn't use the native typemap tables.
// Delegate to the managed TrimmableTypeMap instead.
return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null;
}

if (mvid_bytes == null)
mvid_bytes = new byte[16];

Expand Down
2 changes: 2 additions & 0 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args)
internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args)
{
androidRuntime = runtime;
JniRuntime.SetCurrent (runtime);
SetSynchronizationContext ();
}

Expand Down Expand Up @@ -159,6 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
valueManager,
args->jniAddNativeMethodRegistrationAttributePresent != 0
);
JniRuntime.SetCurrent (androidRuntime);

if (RuntimeFeature.TrimmableTypeMap) {
TrimmableTypeMap.Initialize ();
Expand Down
7 changes: 6 additions & 1 deletion src/Mono.Android/Java.Interop/TypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,12 @@ static Type monovm_typemap_java_to_managed (string java_type_name)
return type;
}

if (RuntimeFeature.IsMonoRuntime) {
if (RuntimeFeature.TrimmableTypeMap) {
throw new System.Diagnostics.UnreachableException (
$"{nameof (TypeManager)}.{nameof (GetJavaToManagedTypeCore)} should not be used when " +
$"{nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. The trimmable path should resolve " +
$"types through {nameof (TrimmableTypeMapTypeManager)}.");
} else if (RuntimeFeature.IsMonoRuntime) {
type = monovm_typemap_java_to_managed (class_name);
} else if (RuntimeFeature.IsCoreClrRuntime) {
type = clr_typemap_java_to_managed (class_name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,18 +507,35 @@ void ProcessContext (HandleContext* context)
[DynamicallyAccessedMembers (Constructors)]
Type? targetType)
{
ThrowIfDisposed ();

if (!reference.IsValid) {
return null;
}

if (RuntimeFeature.TrimmableTypeMap) {
var typeMap = TrimmableTypeMap.Instance;
if (typeMap is not null && targetType is not null) {
var proxy = typeMap.GetProxyForManagedType (targetType);
if (proxy is not null) {
var peer = proxy.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
if (peer is not null) {
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
JniObjectReference.Dispose (ref reference, transfer);
return peer;
try {
var typeMap = TrimmableTypeMap.Instance;
var proxy = typeMap.GetProxyForJavaObject (reference.Handle, targetType);
var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer);
if (peer is not null) {
var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable;
if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) {
peerState |= JniManagedPeerStates.Activatable;
}
peer.SetJniManagedPeerState (peerState);
return peer;
}

var targetName = targetType?.AssemblyQualifiedName ?? "<null>";
var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference);

throw new NotSupportedException (
$"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " +
$"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " +
$"This indicates a missing trimmable typemap proxy or association and should be fixed in the generator.");
} finally {
JniObjectReference.Dispose (ref reference, transfer);
}
}

Expand Down
138 changes: 104 additions & 34 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Microsoft.Android.Runtime;
class TrimmableTypeMap
{
static readonly Lock s_initLock = new ();
static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy ();
static TrimmableTypeMap? s_instance;

internal static TrimmableTypeMap Instance =>
Expand All @@ -28,7 +29,8 @@ class TrimmableTypeMap

readonly IReadOnlyDictionary<string, Type> _typeMap;
readonly IReadOnlyDictionary<Type, Type> _proxyTypeMap;
readonly ConcurrentDictionary<Type, JavaPeerProxy?> _proxyCache = new ();
readonly ConcurrentDictionary<Type, JavaPeerProxy> _proxyCache = new ();
readonly ConcurrentDictionary<string, JavaPeerProxy> _peerProxyCache = new (StringComparer.Ordinal);

TrimmableTypeMap ()
{
Expand All @@ -55,9 +57,6 @@ internal static void Initialize ()
}
}

/// <summary>
/// Registers the <c>mono.android.Runtime.registerNatives</c> JNI native method.
/// </summary>
unsafe void RegisterNatives ()
{
using var runtimeClass = new JniType ("mono/android/Runtime"u8);
Expand All @@ -68,49 +67,111 @@ unsafe void RegisterNatives ()
}
}

internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type)
=> _typeMap.TryGetValue (jniSimpleReference, out type);
internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type)
{
type = GetProxyForJavaType (jniSimpleReference)?.TargetType;
return type is not null;
}

/// <summary>
/// Finds the proxy for a managed type using the generated proxy type map.
/// Results are cached per type.
/// </summary>
internal JavaPeerProxy? GetProxyForManagedType (Type managedType)
=> _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type), this);
JavaPeerProxy? GetProxyForManagedType (Type managedType)
{
var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => {
if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) {
return s_noPeerSentinel;
}

return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false) ?? s_noPeerSentinel;
}, this);
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
}

JavaPeerProxy? ResolveProxyForManagedType (Type managedType)
JavaPeerProxy? GetProxyForJavaType (string className)
{
if (!_proxyTypeMap.TryGetValue (managedType, out var proxyType)) {
return null;
}
var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => {
if (!self._typeMap.TryGetValue (name, out var mappedType)) {
return s_noPeerSentinel;
}

return proxyType.GetCustomAttribute<JavaPeerProxy> (inherit: false);
var proxy = mappedType.GetCustomAttribute<JavaPeerProxy> (inherit: false);
if (proxy is null) {
// Alias typemap entries (for example "jni/name[1]") are not implemented yet.
// Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788.
throw new NotImplementedException (
$"Trimmable typemap alias handling is not implemented yet for '{name}'.");
}

return proxy;
}, this);
return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy;
}

internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName)
{
var proxy = GetProxyForManagedType (managedType);
if (proxy is not null) {
jniName = proxy.JniName;
return true;
}

jniName = null;
return false;
jniName = GetProxyForManagedType (managedType)?.JniName;
return jniName is not null;
}

/// <summary>
/// Creates a peer instance using the proxy's CreateInstance method.
/// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance.
/// </summary>
internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer)
internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null)
{
var proxy = GetProxyForManagedType (type);
if (proxy is null) {
return false;
if (handle == IntPtr.Zero) {
return null;
}

return proxy.CreateInstance (handle, transfer) != null;
return TryGetProxyFromHierarchy (this, handle, targetType) ??
TryGetProxyFromTargetType (this, handle, targetType);

static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType)
{
var selfRef = new JniObjectReference (handle);
var jniClass = JniEnvironment.Types.GetObjectClass (selfRef);

try {
while (jniClass.IsValid) {
var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass);
if (className != null) {
var proxy = self.GetProxyForJavaType (className);
if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) {
return proxy;
}
}

var super = JniEnvironment.Types.GetSuperclass (jniClass);
JniObjectReference.Dispose (ref jniClass);
jniClass = super;
}
} finally {
JniObjectReference.Dispose (ref jniClass);
}

return null;
}

static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType)
{
if (targetType is null) {
return null;
}

var proxy = self.GetProxyForManagedType (targetType);
// Verify the Java object is actually assignable to the target Java type
// before returning the fallback proxy. Without this, we'd create invalid peers
// (e.g., IAppendableInvoker wrapping a java.lang.Integer).
if (proxy is null || !self.TryGetJniNameForManagedType (targetType, out var targetJniName)) {
return null;
}

var selfRef = new JniObjectReference (handle);
var objClass = default (JniObjectReference);
var targetClass = default (JniObjectReference);
try {
objClass = JniEnvironment.Types.GetObjectClass (selfRef);
targetClass = JniEnvironment.Types.FindClass (targetJniName);
return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null;
} finally {
JniObjectReference.Dispose (ref objClass);
JniObjectReference.Dispose (ref targetClass);
}
}
}

const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
Expand Down Expand Up @@ -162,4 +223,13 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa
}
}

sealed class MissingJavaPeerProxy : JavaPeerProxy
{
public MissingJavaPeerProxy () : base ("<missing>", typeof (Java.Lang.Object), null)
{
}

public override IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) => null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpl
yield return t;
}

if (TrimmableTypeMap.Instance.TryGetType (jniSimpleReference, out var type)) {
if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) {
yield return type;
}
}
Expand Down