Skip to content

Commit 2f9fece

Browse files
committed
[Java.Interop] More sanely dispose of JNI Local References.
JNI Local References are thread-local values, which are cleaned up upon return to the Java VM (assuming there IS a return to the VM). As such, they are not usable across threads; see commit b2f0d6a. A question thus arises [0]: how do we sanely dispose of JNI local references without relying on the GC, as the GC may run on a dedicated finalizer thread, which can't do anything with them (as it's a different thread!). In the ideal world, we could use some Thread-specific cleanup mechanism, e.g. a pthread_key_create(3) destructor (have the destructor call a JniEnvironment method which disposes of registered JniLocalReference values). Unfortunately, .NET doesn't appear to provide any such thread-local cleanup mechanism. We could rely on the Mono embedding API, but I'd rather not require Mono at this time. On the (mistaken) assumption that this was only an issue for newly created threads, I had pondered a JavaVM.CreateThread() method...but that complicates integration with existing Threading code. This can instead be addressed by making four semantic changes to JniEnvironment: 1. Remove JniEnvironment.CheckCurrent(), and replace with a new JniEnvironment(IntPtr) constructor. 2. Alter JniEnvironment.Dispose() to clear all JniLocalReferences associated with the current JniEnvironment. 3. Add a JniEnvironment.SetPendingException(Exception) method. Dispose() will marshal and raise the exception after most other work. 4. Treat JniEnvironment.Current as a "stack" of JniEnvironment values, managed by the JniEnvironment constructor and Dispose() calls. This allows creation of ~arbitrary "scopes", at the end of which all current JNI local references will be cleared. The above changes allow us to alter the pattern for methods registered with Java from: TReturnType (*)(IntPtr jnienv, IntPtr context /* ... args ....*/ ) { JniEnvironment.CheckCurrent (jnienv); try { ... } catch (Exception e) { JniEnvironment.Errors.Throw (e); } } and replace it with: TReturnType (*)(IntPtr jnienv, IntPtr context /* ... args ....*/ ) { var __envp = new JniEnvironment (jnienv); try { ... } catch (Exception e) { __envp.SetPendingException (e); } finally { __envp.Dispose (); } } The new pattern ensures that if we have a C#1 > Java > C#2 callstack, any JNI Local References allocated within the C#2 frame will be cleaned up before returning to the Java frame. This allows conforming code to do a "best effort" at preventing leaks of JNI local references, ensuring cleanup without relying on the GC. Note: As part of removing JniEnvironment.CheckCurrent() and adding the JniEnvironment(IntPtr) constructor, the underlying JniEnvironmentSafeHandle value for the current thread must be static, such that previous instances on the "environment stack" always use the latest handle value: var top = new JniEnvironment (HandleA); // top.SafeHandle.DangerousGetHandle() == HandleA; using (var nested = new JniEnvironment (HandleB)) { // nested.SafeHandle.DangerousGetHandle() == HandleB; } // top.SafeHandle.DangerousGetHandle() == HandleB; This semantic is required for the same reason that JniEnvironment.CheckCurrent() was required: it's possible for the JNIEnv* value for the current thread to change, and we must ALWAYS use the latest version. Note: Java.Interop.Export never needed to call JniEnvironment.CheckCurrent(), because the methods it generates are wrapped by JniType.RegisterNativeMethods() / JniMarshalMethod.Wrap(). It is thus safe to remove those checks. Note: With the new "auto-cleanup" logic, JniReferenceSafeHandle.Null can no longer be a JniLocalReference, as it will be disposed when the thread-local JniEnvironment is disposed (which results in bizarre ObjectDisposedException errors in the unit tests). JniInvocationHandle should have been used instead. [0]: A related question: should we care? Commit b2f0d6a will cause JniLocalReference.Dispose() to be a no-op from outside the thread that created them, so the only overhead is the finalizer queue.
1 parent 633e2d0 commit 2f9fece

File tree

8 files changed

+186
-82
lines changed

8 files changed

+186
-82
lines changed

src/Java.Interop.Export/Java.Interop/ExportedMemberBuilder.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ public virtual LambdaExpression CreateMarshalFromJniMethodExpression (ExportAttr
114114
};
115115

116116
var marshalBody = new List<Expression> () {
117-
CheckJnienv (jnienv),
118117
Expression.Assign (jvm, GetJavaVM ()),
119118
};
120119

@@ -272,12 +271,6 @@ static Func<T, TRet> F<T, TRet> (Func<T, TRet> func)
272271
return func;
273272
}
274273

275-
static Expression CheckJnienv (ParameterExpression jnienv)
276-
{
277-
Action<IntPtr> a = JniEnvironment.CheckCurrent;
278-
return Expression.Call (null, a.Method, jnienv);
279-
}
280-
281274
static Expression GetThis (Expression vm, Type targetType, Expression context)
282275
{
283276
return Expression.Call (

src/Java.Interop.Export/Tests/Java.Interop/ExportedMemberBuilderTest.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ public void CreateMarshalFromJniMethodExpression_InstanceAction ()
170170
JavaVM __jvm;
171171
ExportTest __this;
172172
173-
JniEnvironment.CheckCurrent(__jnienv);
174173
__jvm = JniEnvironment.Current.JavaVM;
175174
__this = __jvm.GetObject<ExportTest>(__context);
176175
__this.InstanceAction();
@@ -212,7 +211,6 @@ public void CreateMarshalFromJniMethodExpression_StaticAction ()
212211
{
213212
JavaVM __jvm;
214213
215-
JniEnvironment.CheckCurrent(__jnienv);
216214
__jvm = JniEnvironment.Current.JavaVM;
217215
ExportTest.StaticAction();
218216
}");
@@ -232,7 +230,6 @@ public void CreateMarshalFromJniMethodExpression_StaticActionInt32String ()
232230
JavaVM __jvm;
233231
string v;
234232
235-
JniEnvironment.CheckCurrent(__jnienv);
236233
__jvm = JniEnvironment.Current.JavaVM;
237234
v = Strings.ToString(native_v);
238235
ExportTest.StaticActionInt32String(i, v);
@@ -255,7 +252,6 @@ public void CreateMarshalFromJniMethodExpression_FuncInt64 ()
255252
long __jret;
256253
long __mret;
257254
258-
JniEnvironment.CheckCurrent(__jnienv);
259255
__jvm = JniEnvironment.Current.JavaVM;
260256
__this = __jvm.GetObject<ExportTest>(__context);
261257
__mret = __this.FuncInt64();
@@ -280,7 +276,6 @@ public void CreateMarshalFromJniMethodExpression_FuncIJavaObject ()
280276
IntPtr __jret;
281277
JavaObject __mret;
282278
283-
JniEnvironment.CheckCurrent(__jnienv);
284279
__jvm = JniEnvironment.Current.JavaVM;
285280
__this = __jvm.GetObject<ExportTest>(__context);
286281
__mret = __this.FuncIJavaObject();

src/Java.Interop/Java.Interop/JavaVM.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ protected JavaVM (JavaVMOptions options)
172172

173173
if (options.EnvironmentHandle != null) {
174174
var env = new JniEnvironment (options.EnvironmentHandle, this);
175-
Track (env);
175+
JniEnvironment.SetRootEnvironment (env);
176176
}
177177

178178
JavaVMs.TryAdd (SafeHandle.DangerousGetHandle (), this);
@@ -218,13 +218,17 @@ protected virtual void Dispose (bool disposing)
218218
ClearTrackedReferences ();
219219
JavaVM _;
220220
JavaVMs.TryRemove (SafeHandle.DangerousGetHandle (), out _);
221+
JniHandleManager.Dispose ();
222+
// TODO: Dispose JniEnvironment.RootEnvironments
223+
// Requires .NET 4.5+
224+
JniEnvironment.RootEnvironments.Dispose ();
221225
if (DestroyVM)
222226
DestroyJavaVM ();
223227
SafeHandle.Dispose ();
224228
SafeHandle = null;
225229
}
226230

227-
public void AttachCurrentThread (string name = null, JniReferenceSafeHandle group = null)
231+
public JniEnvironment AttachCurrentThread (string name = null, JniReferenceSafeHandle group = null)
228232
{
229233
var threadArgs = new JavaVMThreadAttachArgs () {
230234
version = JniVersion.v1_2,
@@ -239,7 +243,7 @@ public void AttachCurrentThread (string name = null, JniReferenceSafeHandle grou
239243
if (r != 0)
240244
throw new NotSupportedException ("AttachCurrentThread returned " + r);
241245
var env = new JniEnvironment (new JniEnvironmentSafeHandle (jnienv), this);
242-
Track (env);
246+
return env;
243247
} finally {
244248
Marshal.FreeHGlobal (threadArgs.name);
245249
}
@@ -287,11 +291,6 @@ internal void Track (JniType value)
287291
TrackedInstances.TryAdd (value.SafeHandle, value);
288292
}
289293

290-
internal void Track (JniEnvironment value)
291-
{
292-
TrackedInstances.TryAdd (value.SafeHandle, value);
293-
}
294-
295294
internal void UnTrack (SafeHandle key)
296295
{
297296
IDisposable _;

src/Java.Interop/Java.Interop/JniEnvironment.cs

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Linq;
45
using System.Runtime.InteropServices;
56
using System.Threading;
67

@@ -42,78 +43,140 @@ public override string ToString ()
4243

4344
public sealed partial class JniEnvironment : IDisposable {
4445

46+
[ThreadStatic]
47+
static JniEnvironmentSafeHandle handle;
48+
49+
[ThreadStatic]
50+
static JniEnvironment current;
51+
52+
JniEnvironment previous;
53+
JavaVM vm;
54+
bool disposed;
55+
Exception pendingException;
56+
4557
internal JniEnvironment (JniEnvironmentSafeHandle safeHandle, JavaVM javaVM)
4658
{
47-
SafeHandle = safeHandle;
59+
handle = safeHandle;
60+
vm = javaVM;
4861
Invoker = SafeHandle.CreateInvoker ();
49-
JavaVM = javaVM;
5062

51-
if (current == null)
52-
current = this;
63+
previous = current;
64+
current = this;
5365

5466
using (var t = new JniType ("java/lang/Object"))
5567
Object_toString = t.GetInstanceMethod ("toString", "()Ljava/lang/String;");
5668
using (var t = new JniType ("java/lang/Class"))
5769
Class_getName = t.GetInstanceMethod ("getName", "()Ljava/lang/String;");
5870
}
5971

72+
public JniEnvironment (IntPtr jniEnvironmentHandle)
73+
: this (GetCurrentHandle (jniEnvironmentHandle), null)
74+
{
75+
}
76+
77+
static JniEnvironmentSafeHandle GetCurrentHandle (IntPtr jniEnvironmentHandle)
78+
{
79+
if (handle == null)
80+
return new JniEnvironmentSafeHandle (jniEnvironmentHandle);
81+
82+
if (handle.DangerousGetHandle () == jniEnvironmentHandle)
83+
return handle;
84+
85+
handle.Dispose ();
86+
handle = null;
87+
return new JniEnvironmentSafeHandle (jniEnvironmentHandle);
88+
}
89+
6090
internal JniEnvironmentInvoker Invoker;
6191

62-
public JniEnvironmentSafeHandle SafeHandle {get; private set;}
63-
public JavaVM JavaVM {get; private set;}
92+
public JniEnvironmentSafeHandle SafeHandle {
93+
get {return handle ?? RootEnvironment.SafeHandle;}
94+
}
95+
96+
public JavaVM JavaVM {
97+
get {
98+
if (vm != null)
99+
return vm;
100+
101+
JavaVMSafeHandle vmh;
102+
int r = Invoker.GetJavaVM (SafeHandle, out vmh);
103+
if (r < 0)
104+
throw new InvalidOperationException ("JNIEnv::GetJavaVM() returned: " + r);
105+
106+
vm = JavaVM.GetRegisteredJavaVM (vmh);
107+
if (vm == null)
108+
throw new NotSupportedException (
109+
string.Format ("No JavaVM registered with handle 0x{0}.",
110+
vmh.DangerousGetHandle ().ToString ("x")));
111+
112+
return vm;
113+
}
114+
}
64115

65116
List<JniLocalReference> lrefs;
66117
internal List<JniLocalReference> LocalReferences {
67118
get {return lrefs ?? (lrefs = new List<JniLocalReference> ());}
68119
}
69120

70-
[ThreadStatic]
71-
static JniEnvironment current;
72121
public static JniEnvironment Current {
73122
get {
74-
if (current != null)
75-
return current;
76-
JavaVM.Current.AttachCurrentThread ();
77-
return current;
123+
return current ?? RootEnvironment;
78124
}
79125
}
80126

81-
public static void CheckCurrent (IntPtr jniEnvironmentHandle)
82-
{
83-
if (current != null && current.SafeHandle.DangerousGetHandle () == jniEnvironmentHandle)
84-
return;
85-
if (current != null)
86-
current.Dispose ();
87-
current = null;
88-
89-
JavaVMSafeHandle vm;
127+
internal static ThreadLocal<JniEnvironment> RootEnvironments = new ThreadLocal<JniEnvironment> (
128+
() => JavaVM.Current.AttachCurrentThread ()
129+
);
90130

91-
var h = new JniEnvironmentSafeHandle (jniEnvironmentHandle);
92-
var i = h.CreateInvoker ();
93-
int r = i.GetJavaVM (h, out vm);
94-
if (r < 0)
95-
throw new InvalidOperationException ("JNIEnv::GetJavaVM() returned: " + r);
131+
public static JniEnvironment RootEnvironment {
132+
get {
133+
return RootEnvironments.Value ??
134+
(RootEnvironments.Value = JavaVM.Current.AttachCurrentThread ());
135+
}
136+
}
96137

97-
var jvm = JavaVM.GetRegisteredJavaVM (vm);
98-
if (jvm == null)
99-
throw new NotSupportedException (
100-
string.Format ("No JavaVM registered with handle 0x{0}.",
101-
vm.DangerousGetHandle ().ToString ("x")));
138+
internal static void SetRootEnvironment (JniEnvironment environment)
139+
{
140+
RootEnvironments.Value = environment;
141+
}
102142

103-
new JniEnvironment (h, jvm);
143+
public void SetPendingException (Exception e)
144+
{
145+
pendingException = e;
104146
}
105147

106148
public void Dispose ()
107149
{
108-
if (SafeHandle == null || SafeHandle.IsInvalid)
150+
if (disposed || handle == null || handle.IsInvalid)
109151
return;
152+
153+
disposed = true;
154+
155+
if (lrefs != null) {
156+
// Copy required as lref.Dispose() calls DeleteLocalReference(), alters lrefs.
157+
var refs = lrefs.ToList ();
158+
foreach (var lref in refs) {
159+
// check required due to https://bugzilla.xamarin.com/show_bug.cgi?id=25850
160+
if (!lref.IsClosed)
161+
lref.Dispose ();
162+
}
163+
lrefs = null;
164+
}
165+
166+
if (pendingException != null)
167+
Errors.Throw (pendingException);
168+
110169
Object_toString.Dispose ();
111-
if (current == this)
112-
current = null;
113-
JavaVM.UnTrack (SafeHandle);
114-
SafeHandle.Dispose ();
115-
SafeHandle = null;
116-
JavaVM = null;
170+
Class_getName.Dispose ();
171+
172+
if ((previous == null && !RootEnvironments.IsValueCreated) ||
173+
(RootEnvironments.IsValueCreated && RootEnvironment == this)) {
174+
handle.Dispose ();
175+
handle = null;
176+
RootEnvironments.Value = null;
177+
}
178+
179+
current = previous;
117180
}
118181

119182
public JniVersion JniVersion {
@@ -123,7 +186,13 @@ public JniVersion JniVersion {
123186
internal int LrefCount;
124187

125188
public int LocalReferenceCount {
126-
get {return LrefCount;}
189+
get {
190+
int lc = 0;
191+
for (var c = this; c != null; c = c.previous) {
192+
lc += c.LrefCount;
193+
}
194+
return lc;
195+
}
127196
}
128197

129198
internal void LogCreateLocalRef (JniLocalReference value)

0 commit comments

Comments
 (0)