Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9c4067a
[Java.Interop] Release method-cache redirect references
simonrozsival Sep 11, 2026
588c8fc
[Java.Interop] Dispose subclass caches without initializing the owner
simonrozsival Sep 11, 2026
cf8a268
Fix transferred JNI reference cleanup during activation
simonrozsival Sep 11, 2026
e8c1596
[runtime] Release temporary startup class reference
simonrozsival Sep 11, 2026
beff091
Dispose unpublished subclass constructor caches
simonrozsival Sep 11, 2026
c17a279
[Java.Interop] Preserve standalone native registration ownership
simonrozsival Sep 11, 2026
94a8991
[Mono.Android] Avoid retaining canceled Action callbacks
simonrozsival Sep 11, 2026
7e68e0e
[tests] Keep standalone registration fixture off Android
simonrozsival Sep 11, 2026
bea36b7
[tests] Make gref regressions reliable across test runtimes
simonrozsival Sep 11, 2026
9cd00ba
[tests] Tolerate JDK-specific module constant pools
simonrozsival Sep 11, 2026
32f9b47
[tests] Clean up bytecode test formatting
simonrozsival Sep 11, 2026
37f9bbf
[Java.Interop] Keep empty native registration a no-op
simonrozsival Sep 11, 2026
addc53d
[Java.Interop] Preserve empty-registration formatting
simonrozsival Sep 11, 2026
efb7394
[Java.Interop] Explain method cache publication
simonrozsival Sep 11, 2026
8af77b4
[Mono.Android] Avoid callback removal closure
simonrozsival Sep 11, 2026
c77de9a
[tests] Keep native registration coverage focused
simonrozsival Sep 11, 2026
b35b5cb
[Java.Interop] Explain native registration roots
simonrozsival Sep 11, 2026
30ced49
[Java.Interop] Clarify registration retention naming
simonrozsival Sep 11, 2026
b43909a
[Java.Interop] Encapsulate method cache ownership
simonrozsival Sep 11, 2026
8d5d846
[Java.Interop] Configure method cache at call sites
simonrozsival Sep 11, 2026
0fad335
[Java.Interop] Size custom method caches explicitly
simonrozsival Sep 11, 2026
6c49220
[Java.Interop] Dispose unused cache candidates
simonrozsival Sep 11, 2026
f2e6a46
[Java.Interop] Use Lock for native registrations
simonrozsival Sep 11, 2026
caf9a29
[Java.Interop] Reject repeated native registration
simonrozsival Sep 11, 2026
4051811
[Java.Interop] Test redirect cleanup through public API
simonrozsival Sep 11, 2026
a5c8e7a
[Java.Interop] Generalize resource-owning cache
simonrozsival Sep 11, 2026
a90470a
[Java.Interop] Atomically claim native registration
simonrozsival Sep 11, 2026
5fcfc48
[Java.Interop] Clarify repeated registration error
simonrozsival Sep 11, 2026
341b06b
[Java.Interop] Keep registration ownership in JniType
simonrozsival Sep 11, 2026
0ace082
[Java.Interop] Explain registration retention order
simonrozsival Sep 11, 2026
2bc7c2a
[Java.Interop] Encapsulate runtime ownership cleanup
simonrozsival Sep 11, 2026
13088c8
[Mono.Android] Avoid callback removal closures
simonrozsival Sep 11, 2026
8c55d90
[Java.Interop] Delay registration ownership transfer
simonrozsival Sep 11, 2026
2424be1
[Java.Interop] Pass native registration owner directly
simonrozsival Sep 11, 2026
70c79a8
[Java.Interop] Track native registration before marshaling
simonrozsival Sep 11, 2026
92eeb00
[Java.Interop] Fix cache ownership CI failures
simonrozsival Sep 11, 2026
ad5c018
[Java.Interop] Preserve repeated native registration
simonrozsival Sep 11, 2026
c498384
[Mono.Android] Keep runnable removal references valid
simonrozsival Sep 16, 2026
379c1af
Merge origin/main into simonrozsival-gref-leak-audit
simonrozsival Sep 17, 2026
b7b008a
[Java.Interop] Synchronize JNI ownership teardown
simonrozsival Sep 17, 2026
42194cf
[Java.Interop] Document member cache teardown contract
simonrozsival Sep 18, 2026
dc7dc71
[Java.Interop] Retain private native registration batches
simonrozsival Sep 18, 2026
da5b253
[Mono.Android] Preserve callback binding semantics
simonrozsival Sep 18, 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
@@ -1,7 +1,6 @@
#nullable enable

using System;
using System.Collections.Concurrent;

namespace Java.Interop
{
Expand Down Expand Up @@ -39,16 +38,16 @@ internal JniType JniPeerType {

readonly Type DeclaringType;

ConcurrentDictionary<string, JniMethodInfo>? instanceMethods;
ConcurrentDictionary<Type, JniInstanceMethods>? subclassConstructors;
JniValueCache<string, JniMethodInfo>? instanceMethods;
JniValueCache<Type, JniInstanceMethods>? subclassConstructors;

ConcurrentDictionary<string, JniMethodInfo> InstanceMethods => GetOrCreate (ref instanceMethods, 3);
ConcurrentDictionary<Type, JniInstanceMethods> SubclassConstructors => GetOrCreate (ref subclassConstructors, 1);
JniValueCache<string, JniMethodInfo> InstanceMethods => JniValueCache<string, JniMethodInfo>.GetOrCreate (ref instanceMethods, 1, 3, static value => value.StaticRedirect?.Dispose ());
JniValueCache<Type, JniInstanceMethods> SubclassConstructors => JniValueCache<Type, JniInstanceMethods>.GetOrCreate (ref subclassConstructors, 1, 1, static value => value.Dispose ());

internal void Dispose ()
{
Clear (ref instanceMethods);
Clear (ref subclassConstructors, static value => value.Dispose ());
JniValueCache<string, JniMethodInfo>.Dispose (ref instanceMethods);
JniValueCache<Type, JniInstanceMethods>.Dispose (ref subclassConstructors);

if (jniPeerType != null)
jniPeerType.Dispose ();
Expand Down Expand Up @@ -111,15 +110,20 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signa
var methodName = newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method;
var methodSig = newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature;

using var t = new JniType (typeName);
if (newMethod.Value.TargetJniMethodInstanceToStatic &&
t.TryGetStaticMethod (methodName, methodSig, out m)) {
m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount;
m.StaticRedirect = new JniType (typeName);
return m;
}
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
return m;
JniType? t = new JniType (typeName);
try {
if (newMethod.Value.TargetJniMethodInstanceToStatic &&
t.TryGetStaticMethod (methodName, methodSig, out m)) {
m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount;
m.StaticRedirect = t;
t = null;
return m;
}
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
return m;
}
} finally {
t?.Dispose ();
}
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#nullable enable

using System;
using System.Collections.Concurrent;

namespace Java.Interop
{
Expand All @@ -15,13 +14,13 @@ internal JniStaticMethods (JniPeerMembers members)

internal readonly JniPeerMembers Members;

ConcurrentDictionary<string, JniMethodInfo>? staticMethods;
JniValueCache<string, JniMethodInfo>? staticMethods;

ConcurrentDictionary<string, JniMethodInfo> StaticMethods => GetOrCreate (ref staticMethods, 3);
JniValueCache<string, JniMethodInfo> StaticMethods => JniValueCache<string, JniMethodInfo>.GetOrCreate (ref staticMethods, 1, 3, static value => value.StaticRedirect?.Dispose ());

internal void Dispose ()
{
Clear (ref staticMethods);
JniValueCache<string, JniMethodInfo>.Dispose (ref staticMethods);
}

public JniMethodInfo GetMethodInfo (string encodedMember)
Expand All @@ -38,12 +37,20 @@ JniMethodInfo GetMethodInfo (ReadOnlySpan<char> method, ReadOnlySpan<char> signa
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
if (newMethod.HasValue) {
using var t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName);
if (t.TryGetStaticMethod (
newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method,
newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature,
out m)) {
return m;
JniType? t = new JniType (newMethod.Value.TargetJniType ?? Members.JniPeerTypeName);
try {
if (t.TryGetStaticMethod (
newMethod.Value.TargetJniMethodName is string name ? name.AsSpan () : method,
newMethod.Value.TargetJniMethodSignature is string sig ? sig.AsSpan () : signature,
out m)) {
if (!JniEnvironment.Types.IsSameObject (t.PeerReference, Members.JniPeerType.PeerReference)) {
m.StaticRedirect = t;
t = null;
}
return m;
}
} finally {
t?.Dispose ();
}
}
if (Members.JniPeerType.TryGetStaticMethod (method, signature, out m)) {
Expand Down Expand Up @@ -72,23 +79,28 @@ JniType GetMethodDeclaringType (JniMethodInfo method)
if (fallbackTypes == null) {
return null;
}
foreach (var ft in fallbackTypes) {
JniType? t = null;
try {
JniType? t = null;
try {
JniMethodInfo? m = null;
foreach (var ft in fallbackTypes) {
if (!JniType.TryParse (ft, out t)) {
continue;
}
if (t.TryGetStaticMethod (method, signature, out var m)) {
m.StaticRedirect = t;
t = null;
return m;
if (t.TryGetStaticMethod (method, signature, out m)) {
break;
}
t.Dispose ();
t = null;
}
finally {
t?.Dispose ();
if (m != null) {
// Transfer ownership only after the fallback enumerator has been disposed.
m.StaticRedirect = t;
t = null;
}
return m;
} finally {
t?.Dispose ();
}
return null;
}

public unsafe void InvokeVoidMethod (string encodedMember, JniArgumentValue* parameters)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#nullable enable

using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

namespace Java.Interop {

partial class JniPeerMembers {

internal sealed class JniValueCache<TKey, TValue> : IDisposable, IReadOnlyDictionary<TKey, TValue>
where TKey : notnull
where TValue : class
{

readonly ConcurrentDictionary<TKey, TValue> values;
readonly Action<TValue> dispose;
// JniPeerMembers disposal requires external synchronization with users of returned
// values. This state only prevents in-flight factories from publishing into a
// detached cache after teardown.
int disposed;

public JniValueCache (int concurrencyLevel, int capacity, Action<TValue> dispose)
{
values = new ConcurrentDictionary<TKey, TValue> (concurrencyLevel, capacity);
this.dispose = dispose;
}

public int Count => values.Count;
public IEnumerable<TKey> Keys => values.Keys;
public IEnumerable<TValue> Values => values.Values;
public TValue this [TKey key] => values [key];

public bool ContainsKey (TKey key) => values.ContainsKey (key);
public bool TryGetValue (TKey key, [MaybeNullWhen (false)] out TValue value) => values.TryGetValue (key, out value);

public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator () => values.GetEnumerator ();
IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();

internal static JniValueCache<TKey, TValue> GetOrCreate (ref JniValueCache<TKey, TValue>? cache, int concurrencyLevel, int capacity, Action<TValue> dispose)
{
var value = Volatile.Read (ref cache);
if (value != null)
return value;

var candidate = new JniValueCache<TKey, TValue> (concurrencyLevel, capacity, dispose);
var existing = Interlocked.CompareExchange (ref cache, candidate, null);
if (existing == null)
return candidate;

candidate.Dispose ();
return existing;
}

internal static void Dispose (ref JniValueCache<TKey, TValue>? cache)
{
Interlocked.Exchange (ref cache, null)?.Dispose ();
}

public TValue GetOrAdd (TKey key, Func<TKey, TValue> factory)
{
return GetOrAdd (key, static (key, factory) => factory (key), factory);
}

public TValue GetOrAdd<TArg> (TKey key, Func<TKey, TArg, TValue> factory, TArg argument)
{
AssertNotDisposed ();

if (values.TryGetValue (key, out var value))
Comment thread
simonrozsival marked this conversation as resolved.
return value;

// ConcurrentDictionary may invoke a GetOrAdd factory multiple times and discard
// losing values. Construct explicitly so an unpublished owner can be disposed.
// JNI lookup can also reenter this cache, so do not lock construction.
TValue? candidate = factory (key, argument);
try {
AssertNotDisposed ();
value = values.GetOrAdd (key, candidate);
if (ReferenceEquals (value, candidate))
candidate = null;
if (Volatile.Read (ref disposed) != 0) {
if (values.TryRemove (key, out var removed))
dispose (removed);
throw new ObjectDisposedException (nameof (JniValueCache<TKey, TValue>));
}
return value;
} finally {
if (candidate != null)
dispose (candidate);
}
}

void AssertNotDisposed ()
{
if (Volatile.Read (ref disposed) != 0)
throw new ObjectDisposedException (nameof (JniValueCache<TKey, TValue>));
}

public void Dispose ()
{
if (Interlocked.Exchange (ref disposed, 1) != 0)
return;

foreach (var key in values.Keys) {
if (values.TryRemove (key, out var value))
dispose (value);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,26 @@ static void Clear<TKey, TValue> (ref ConcurrentDictionary<TKey, TValue>? diction

protected virtual void Dispose (bool disposing)
{
if (!disposing || jniPeerType == null)
if (!disposing)
return;

instanceMethods.Dispose ();
instanceFields.Dispose ();
staticMethods.Dispose ();
staticFields.Dispose ();
jniPeerType.Dispose ();
jniPeerType?.Dispose ();

jniPeerType = null;
}

/// <summary>
/// Releases JNI resources cached by <paramref name="members"/>.
/// </summary>
/// <remarks>
/// The caller must ensure that no member lookup, invocation, or previously returned
/// cached value is in use while this method executes. Later access can recreate the
/// released caches.
/// </remarks>
public static void Dispose (JniPeerMembers members)
{
if (members == null)
Expand Down
Loading
Loading