Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ internal JniInstanceFields (JniPeerMembers members)

readonly JniPeerMembers Members;

readonly ConcurrentDictionary<string, JniFieldInfo> InstanceFields = new ConcurrentDictionary<string, JniFieldInfo> (1, 3, StringComparer.Ordinal);
ConcurrentDictionary<string, JniFieldInfo>? instanceFields;

ConcurrentDictionary<string, JniFieldInfo> InstanceFields => GetOrCreate (ref instanceFields, 3);

internal void Dispose ()
{
InstanceFields.Clear ();
Clear (ref instanceFields);
}

public JniFieldInfo GetFieldInfo (string encodedMember)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@ internal JniType JniPeerType {

readonly Type DeclaringType;

readonly ConcurrentDictionary<string, JniMethodInfo> InstanceMethods = new ConcurrentDictionary<string, JniMethodInfo> (1, 3, StringComparer.Ordinal);
readonly ConcurrentDictionary<Type, JniInstanceMethods> SubclassConstructors = new ConcurrentDictionary<Type, JniInstanceMethods> (1, 1);
ConcurrentDictionary<string, JniMethodInfo>? instanceMethods;
ConcurrentDictionary<Type, JniInstanceMethods>? subclassConstructors;

ConcurrentDictionary<string, JniMethodInfo> InstanceMethods => GetOrCreate (ref instanceMethods, 3);
ConcurrentDictionary<Type, JniInstanceMethods> SubclassConstructors => GetOrCreate (ref subclassConstructors, 1);

internal void Dispose ()
{
InstanceMethods.Clear ();
foreach (var p in SubclassConstructors.Values)
p.Dispose ();
SubclassConstructors.Clear ();
Clear (ref instanceMethods);
Clear (ref subclassConstructors, static value => value.Dispose ());

if (jniPeerType != null)
jniPeerType.Dispose ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ internal JniStaticFields (JniPeerMembers members)

readonly JniPeerMembers Members;

readonly ConcurrentDictionary<string, JniFieldInfo> StaticFields = new ConcurrentDictionary<string, JniFieldInfo> (1, 3, StringComparer.Ordinal);
ConcurrentDictionary<string, JniFieldInfo>? staticFields;

ConcurrentDictionary<string, JniFieldInfo> StaticFields => GetOrCreate (ref staticFields, 3);

public JniFieldInfo GetFieldInfo (string encodedMember)
{
Expand All @@ -28,7 +30,7 @@ public JniFieldInfo GetFieldInfo (string encodedMember)

internal void Dispose ()
{
StaticFields.Clear ();
Clear (ref staticFields);
}
}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ internal JniStaticMethods (JniPeerMembers members)

internal readonly JniPeerMembers Members;

readonly ConcurrentDictionary<string, JniMethodInfo> StaticMethods = new ConcurrentDictionary<string, JniMethodInfo> (1, 3, StringComparer.Ordinal);
ConcurrentDictionary<string, JniMethodInfo>? staticMethods;

ConcurrentDictionary<string, JniMethodInfo> StaticMethods => GetOrCreate (ref staticMethods, 3);

internal void Dispose ()
{
StaticMethods.Clear ();
Clear (ref staticMethods);
}

public JniMethodInfo GetMethodInfo (string encodedMember)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#nullable enable

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

namespace Java.Interop {

Expand Down Expand Up @@ -108,6 +110,30 @@ static T Assert<T>(T value)
return value;
}

static ConcurrentDictionary<TKey, TValue> GetOrCreate<TKey, TValue> (ref ConcurrentDictionary<TKey, TValue>? dictionary, int capacity)
where TKey : notnull
{
var value = Volatile.Read (ref dictionary);
if (value != null)
return value;

var candidate = new ConcurrentDictionary<TKey, TValue> (1, capacity);
return Interlocked.CompareExchange (ref dictionary, candidate, null) ?? candidate;
}

static void Clear<TKey, TValue> (ref ConcurrentDictionary<TKey, TValue>? dictionary, Action<TValue>? dispose = null)
where TKey : notnull
{
var values = Interlocked.Exchange (ref dictionary, null);
if (values == null)
return;
if (dispose != null) {
foreach (var value in values.Values)
dispose (value);
}
values.Clear ();
}

protected virtual void Dispose (bool disposing)
{
if (!disposing || jniPeerType == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;

using Java.Interop;
using NUnit.Framework;
Expand All @@ -21,19 +22,148 @@ public void Ctor_CanReferenceNonexistentType ()
[Category ("TrimmableTypeMapUnsupported")]
public void VirtualInvokeOnBaseInvokesMostDerivedJavaMethod ()
{
var registered = GetInstanceMethods (MyString._members.InstanceMethods);
Assert.AreEqual (0, registered.Count);
Assert.IsNull (GetInstanceMethods (MyString._members.InstanceMethods));
using (var s = new MyString ("hello!")) {
var registered = GetInstanceMethods (MyString._members.InstanceMethods);
Assert.AreEqual (1, registered.Count); // for the constructor
Assert.AreEqual ("hello!", s.ToString ());
Assert.AreEqual (1, registered.Count);
}
}

[Test]
[Category ("TrimmableTypeMapUnsupported")]
public void ConcurrentFirstUsePublishesSingleInstanceMethodCache ()
{
var members = new JniPeerMembers (MyString.JniTypeName, typeof (MyString));
try {
var methods = members.InstanceMethods;
var constructors = new JniMethodInfo [16];

Assert.IsNull (GetInstanceMethods (methods));
Parallel.For (0, constructors.Length, i => constructors [i] = methods.GetConstructor ("()V"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Testing — Please synchronize the workers before they call GetConstructor() (for example with a Barrier) so they are guaranteed to contend on the initial null cache. Parallel.For may execute these short iterations serially or only after the first iteration has already published the dictionary, allowing a broken non-atomic initializer to pass nondeterministically.

Rule: Deterministic concurrency coverage


var registered = GetInstanceMethods (methods);
Assert.AreEqual (1, registered.Count);
foreach (var constructor in constructors)
Assert.AreSame (constructors [0], constructor);
Assert.AreSame (registered ["()V"], constructors [0]);
} finally {
JniPeerMembers.Dispose (members);
}
}

[Test]
public void PeerMemberCachesAreInitiallyNull ()
{
var members = new JniPeerMembers (CallNonvirtualBase.JniTypeName, typeof (CallNonvirtualBase));
try {
Assert.IsNull (GetInstanceFields (members.InstanceFields));
Assert.IsNull (GetInstanceMethods (members.InstanceMethods));
Assert.IsNull (GetSubclassConstructors (members.InstanceMethods));
Assert.IsNull (GetStaticFields (members.StaticFields));
Assert.IsNull (GetStaticMethods (members.StaticMethods));
} finally {
JniPeerMembers.Dispose (members);
}
}

[Test]
public void ConstructorTypeCacheIsAllocatedOnlyForManagedSubclasses ()
{
var members = new JniPeerMembers (CallNonvirtualBase.JniTypeName, typeof (CallNonvirtualBase));
try {
var methods = members.InstanceMethods;

Assert.AreSame (methods, methods.GetConstructorsForType (typeof (CallNonvirtualBase)));
Assert.IsNull (GetSubclassConstructors (methods));

var derivedMethods = methods.GetConstructorsForType (typeof (CallNonvirtualDerived));
var constructors = GetSubclassConstructors (methods);
Assert.AreEqual (1, constructors.Count);
Assert.AreSame (derivedMethods, constructors [typeof (CallNonvirtualDerived)]);

methods.Dispose ();
Assert.IsNull (GetSubclassConstructors (methods));
Assert.Throws<InvalidOperationException> (() => {
var type = derivedMethods.JniPeerType;
});
} finally {
JniPeerMembers.Dispose (members);
}
}

[Test]
public void ConcurrentFirstUsePublishesSingleFieldAndStaticMethodCaches ()
{
var instanceMembers = new JniPeerMembers (CallNonvirtualBase.JniTypeName, typeof (CallNonvirtualBase));
try {
var instanceFields = new JniFieldInfo [16];
Assert.IsNull (GetInstanceFields (instanceMembers.InstanceFields));
Parallel.For (0, instanceFields.Length, i => instanceFields [i] = instanceMembers.InstanceFields.GetFieldInfo ("methodInvoked.Z"));
AssertSingleCachedValue (GetInstanceFields (instanceMembers.InstanceFields), "methodInvoked.Z", instanceFields);
} finally {
JniPeerMembers.Dispose (instanceMembers);
}

var staticMembers = new JniPeerMembers (JavaLangSystemTestObject.JniTypeName, typeof (JavaLangSystemTestObject));
try {
var staticFields = new JniFieldInfo [16];
Assert.IsNull (GetStaticFields (staticMembers.StaticFields));
Parallel.For (0, staticFields.Length, i => staticFields [i] = staticMembers.StaticFields.GetFieldInfo ("in.Ljava/io/InputStream;"));
AssertSingleCachedValue (GetStaticFields (staticMembers.StaticFields), "in.Ljava/io/InputStream;", staticFields);

var staticMethods = new JniMethodInfo [16];
Assert.IsNull (GetStaticMethods (staticMembers.StaticMethods));
Parallel.For (0, staticMethods.Length, i => staticMethods [i] = staticMembers.StaticMethods.GetMethodInfo ("currentTimeMillis.()J"));
AssertSingleCachedValue (GetStaticMethods (staticMembers.StaticMethods), "currentTimeMillis.()J", staticMethods);
} finally {
JniPeerMembers.Dispose (staticMembers);
}
}

static void AssertSingleCachedValue<T> (ConcurrentDictionary<string, T> cache, string key, T [] values)
where T : class
{
Assert.AreEqual (1, cache.Count);
foreach (var value in values)
Assert.AreSame (values [0], value);
Assert.AreSame (cache [key], values [0]);
}

static ConcurrentDictionary<string, JniFieldInfo> GetInstanceFields (JniPeerMembers.JniInstanceFields fields)
{
var field = typeof (JniPeerMembers.JniInstanceFields).GetField ("instanceFields", BindingFlags.NonPublic | BindingFlags.Instance);
return GetCache<string, JniFieldInfo> (field, fields);
}

static ConcurrentDictionary<string, JniMethodInfo> GetInstanceMethods (JniPeerMembers.JniInstanceMethods methods)
{
var f = typeof (JniPeerMembers.JniInstanceMethods).GetField ("InstanceMethods", BindingFlags.NonPublic | BindingFlags.Instance);
return (ConcurrentDictionary<string, JniMethodInfo>) f.GetValue (methods);
var field = typeof (JniPeerMembers.JniInstanceMethods).GetField ("instanceMethods", BindingFlags.NonPublic | BindingFlags.Instance);
return GetCache<string, JniMethodInfo> (field, methods);
}

static ConcurrentDictionary<Type, JniPeerMembers.JniInstanceMethods> GetSubclassConstructors (JniPeerMembers.JniInstanceMethods methods)
{
var field = typeof (JniPeerMembers.JniInstanceMethods).GetField ("subclassConstructors", BindingFlags.NonPublic | BindingFlags.Instance);
return GetCache<Type, JniPeerMembers.JniInstanceMethods> (field, methods);
}

static ConcurrentDictionary<string, JniFieldInfo> GetStaticFields (JniPeerMembers.JniStaticFields fields)
{
var field = typeof (JniPeerMembers.JniStaticFields).GetField ("staticFields", BindingFlags.NonPublic | BindingFlags.Instance);
return GetCache<string, JniFieldInfo> (field, fields);
}

static ConcurrentDictionary<string, JniMethodInfo> GetStaticMethods (JniPeerMembers.JniStaticMethods methods)
{
var field = typeof (JniPeerMembers.JniStaticMethods).GetField ("staticMethods", BindingFlags.NonPublic | BindingFlags.Instance);
return GetCache<string, JniMethodInfo> (field, methods);
}

static ConcurrentDictionary<TKey, TValue> GetCache<TKey, TValue> (FieldInfo field, object owner)
{
return (ConcurrentDictionary<TKey, TValue>) field.GetValue (owner);
}

[Test]
Expand Down Expand Up @@ -168,6 +298,11 @@ public void DesugarInterfaceStaticMethod ()
}
}

[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
abstract class JavaLangSystemTestObject : JavaObject {
internal const string JniTypeName = "java/lang/System";
}

[JniTypeSignature (JniTypeName, GenerateJavaPeer=false)]
class MyString : JavaObject {
internal const string JniTypeName = "java/lang/String";
Expand Down