Skip to content

Commit 25de1f3

Browse files
committed
[Java.Interop] Expunge SafeHandles from the public API.
(The mother of all commits!) Context: https://trello.com/c/1HLPYTWt/61-optimize-method-invocation Context: https://bugzilla.xamarin.com/show_bug.cgi?id=32126 Xamarin.Android needs a form of JniPeerMembers to cache jmethodIDs for JNIEnv::CallNonvirtual*Method() invocations and constructors. There are two ways to resolve this: 1. Copy/"fork" JniPeerMembers into Xamarin.Android. 2. Update Xamarin.Android to use Java.Interop. (1) is ugly, and makes a possible future migration more difficult -- either we'd need to use different type names, or we'd need to use type forwards. It's a possible mess, and I'd rather avoid it. Which leaves (2): Get Java.Interop to a point that Xamarin.Android can bundle it and use it. Java.Interop is nowhere near that; it's too incomplete. We could, however, create a JniPeerMembers-using *subset* of Java.Interop suitable for use by Xamarin.Android. The problem? JniPeerInstanceMethods.CallObjectMethod() (and many related methods!) need to return a JNI Local Reference, and since the beginning (commit e646783) Java.Interop has used SafeHandles for this. Which doesn't sound like a problem, except for two matters: 1. @jassmith has been profiling Xamarin.Android, and GC allocations would be "a problem", as increased allocations will increase GC-related overheads, slowing down the app. We want a "GC steady state" wherein once things are running, and things are cached, future JNI invocations such as JNIEnv::CallObjectMethod() won't allocate additional garbage (unless *Java* is returning new "garbage" instances...) 2. SafeHandles are thread-safe: http://blogs.msdn.com/b/bclteam/archive/2005/03/16/396900.aspx (2) is a major performance killer. BEST CASE, it *adds* ~40% to runtime execution over an "equivalent" struct-based API. Normal case, JNIEnv::ExceptionOccurred() needs to be invoked, and SafeHandles add *360%+* to the exeuction (wall clock) time. (See README.md and tests/invocation-overhead.) Whatever form of Java.Interop is used by Xamarin.Android, SafeHandles CANNOT be a part of it. The performance impact is unnacceptable. What's the fix, then? Follow Xamarn.Android and use IntPtrs everywhere? While this is possible, the very idea fills me with dread. The Xamarin.Android JNIEnv API is FUGLY, and all the IntPtrs are a significant part of that. (The lack of "grouping" of of related methods such as JniEnvironment.Types is another.) The fix, then? The problem is SafeHandles in particular, and GC-allocated values in general, so avoid them by using a struct: JniObjectReference, which represents a `jobject` -- a JNI local reference, global reference, or weak global reference value. Publicly, it's an immutable value type, much like an IntPtr, but it also "knows" its type, removing the need for the ugly Xamarin.Android JniHandleOwnership.TransferLocalRef and JniHandleOwnership.TransferGlobalRef enum values: public strict JniObjectReference { public IntPtr Handle {get;} public JniObjectReferenceType Type {get;} public JniObjectReference (IntPtr reference, JniObjectReferenceType type); } Where JniObjectReferenceType is what was formerly known as JniReferenceType: public enum JniObjectReferenceType { Invalid, Local, Global, WeakGlobal, } The one "problem" with this is that there's no type distinction between Local, Global, and Weak Global references: instead, they're all JniObjectReference, so improperly intermixing them isn't a compiler type error, it's a runtime error. This is not desirable, but Xamarin.Android has worked this way for *years*, so it's liveable. Another problem is that an IntPtr-based JniObjectReference implementation can't support garbage collecting errant object references, which is also sucky, but (again) Xamarin.Android has been living with that same restriction for years, so this is acceptable. Not ideal, certainly -- ideal would be SafeHandles performing as fast as structs and a GC fast enough that overhead isn't a concern -- but merely acceptable. (For now, internally, JniObjectReference IS using SafeHandles, when FEATURE_HANDLES_ARE_SAFE_HANDLES is #defined. This is done to support long-term performance comparison of handle implementations, and to simplify migrating the unit tests, which DO expect a GC!) With a new core "abstraction" in JniObjectReference, we get a corresponding change in naming: `SafeHandle` is no longer a useful name, because it's not a SafeHandle. I thought of PeerHandle, but that results in ugly `value.PeerHandle.Handle` expressions. Thus, JNI Object References (former JniReferenceSafeHandle and subclasses) are "references", *not* "handles", resulting in e.g. the IJavaObject.PeerReference property (formerly IJavaObject.SafeHandle). With that typing and naming change explained, update tools/jnienv-gen to emit JniEnvironment types that follow this New World Order, and additionally generate an IntPtr-based instead of SafeHandle-based delegate implementation. (This was used in tools/invocation-overhead, but will likely need additional work to actually be usable.) The next "style" change -- which may be a mistake! -- has to do with reference *disposal*. I'd like to prevent "use after free"-style bugs. SafeHandles were ideal for this: since they were reference types, Dispose()ing any variable to a SafeHandle disposed "all" of them, as they all referred to the same instance. That isn't possible with a value type, as assigning between variables copies the struct members. Thus, we could get this: var r = JniEnvironment.Members.CallObjectMethod (...); JniEnvironment.Handles.Dispose (r); Assert.IsTrue (r.IsValid); // *not* expected; `r` has the SAME reference // value, yet is still valid! ARGH! The reason is that because structs are copied by value, passing `r` as a parameter results in a copy, and it's the copy that has its contents updated. It's because of this that I'm wary of having JniObjectReference implement IDisposable: var r = JniEnvironment.Members.CallObjectMethod (...); using (r) { } // `r.Dispose()` called? Assert.IsTrue (r.IsValid); // wat?! A `using` block *copies* the source variable. For reference types, this behaves as you expect, but for struct types it can be "weird": as we see above, `r` IS NOT MODIFIED (but it was in a `using` block!). Though it doesn't *look* it, this is really the same as the previous JniEnvironment.Handles.Dispose() example. This is also why JniObjectReference shouldn't implement IDisposable. In short, an immutable struct makes it *really* easy for "invalid" handles to stick around, so I thought...is there a way to fix this? (Again: this may be a mistake!) The current solution answers the above question with "yes", by using `ref` variables: var r = JniEnvironment.Members.CallObjectMethod (...); JniEnvironment.Handles.Dispose (ref r); Assert.IsFalse (r.IsValid); // Yay! Passing the JNI reference by reference (ugh...?) allows it to be cleared, with the "cleared" state visible to the caller. Explicit copies are still a problem: var r = JniEnvironment.Members.CallObjectMethod (...); var c = r; JniEnvironment.Handles.Dispose (ref r); Assert.IsFalse (r.IsValid); // Yay! Assert.IsTrue (c.IsValid); // Darn! ...but hopefully those won't be common. (Hopefully?) This idea has far-reaching changes: *everything* that previously took a (JniReferenceSafeHandle, JniHandleOwnership) argument pair -- JavaVM.GetObject(), the JavaObject and JavaException constructors -- is now a (ref JniObjectReference, JniHandleOwnership) argument pair. This also complicates method invocations, as new explicit temporaries are required: // Old-and-busted (but nice API!) var name = JniEnvironment.Strings.ToString (Field_getName.CallVirtualObjectMethod (field), JniHandleOwnership.Transfer); // New hawtness? (requires temporary) var n_name = Field_getName.CallVirtualObjectMethod (field); var name = JniEnvironment.Strings.ToString (ref n_name, JniHandleOwnership.Transfer); 99.99% of the time, this is actually a *code generator* problem, not a human one, but on those rare occasions human intervention is warranted...requiring `ref JniObjectReference` is fugly. This idea requires thought. (Fortunately it should be outside of the Xamairn.Android integration API. I hope.) This idea also requires one of the fugliest things I've ever done: dereferencing a null pointer for fun and profit! unsafe partial class JavaObject { protected static readonly JniObjectReference* InvalidJniObjectReference = null; public JavaObject (ref JniObjectReference reference, JniHandleOwnership transfer); } class Subclass : JavaObject { public unsafe Subclass () : base (ref *InvalidJniObjectReference, JniHandleOwnership.Invalid) { } } Look at that! `ref *InvalidJniObjectReference`! A sight to behold! (OMFG am I actually considering this?!) It does work, so long as you don't actually cause the null pointer to be dereferenced...hence the new JniHandleOwnership.Invalid value.
1 parent ce03a52 commit 25de1f3

File tree

94 files changed

+22111
-1808
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+22111
-1808
lines changed

README.md

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ If you fail to re-install the runtime package, then `jar` will fail to run:
6666

6767
## Type Safety
6868

69-
The start of the reboot is to use strongly typed [`SafeHandle`][SafeHandle]
69+
The start of the reboot was to use strongly typed [`SafeHandle`][SafeHandle]
7070
subclasses everywhere instead of `IntPtr`. This allows a local reference to be
7171
type-checked and distinct from a global ref, complete with compiler
7272
type checking.
@@ -76,6 +76,72 @@ type checking.
7676
Since we now have actual types in more places, we can move the current `JNIEnv`
7777
methods into more semantically meaningful types.
7878

79+
Unfortunately, various tests demonstrated that while `SafeHandle`s provided
80+
increased type safety, they did so at a large runtime cost:
81+
82+
1. `SafeHandle`s are reference types, increasing GC heap allocations and pressure.
83+
2. [`SafeHandle`s are *thread-safe* in order to prevent race conditions and handle recycling attacks][reliability].
84+
85+
[reliability]: http://blogs.msdn.com/b/bclteam/archive/2005/03/16/396900.aspx
86+
87+
Compared to a Xamarin.Android-like "use `IntPtr`s for *everything*" binding
88+
approach, the overhread is significant: to *just* invoke
89+
`JNIEnv::CallObjectMethod()`, using `SafeHandle`s for everything causes
90+
execution time to take ~1.4x longer than a comparable struct-oriented approach.
91+
92+
Make the test more realistic -- compared to current Xamarin.Android and
93+
current Java.Interop -- so that `JniEnvironment.Members.CallObjectMethod()`
94+
also calls `JniEnvironment.Errors.ExceptionOccurred()`, which also returns
95+
a JNI local reference -- and runtime execution time *jumped to ~3.6x*:
96+
97+
# SafeHandle timing: 00:00:09.9393493
98+
# Average Invocation: 0.00099393493ms
99+
# JniObjectReference timing: 00:00:02.7254572
100+
# Average Invocation: 0.00027254572ms
101+
102+
(See the [tests/invocation-overhead](tests/invocation-overhead) directory
103+
for the invocation comparison sourcecode.)
104+
105+
*This is not acceptable*. Performance is a known issue with Xamarin.Android;
106+
we can't be making it *worse*.
107+
108+
Meanwhile, I *really* dislike using `IntPtr`s everywhere, as it doesn't let you
109+
know what the value actually represents.
110+
111+
To solve this issue, *avoid `SafeHandle` types* in the public API.
112+
113+
Downside: this means we can't have the GC collect our garbage JNI references.
114+
115+
Upside: the Java.Interop effort will actually be usable.
116+
117+
Instead of using `SafeHandle` types, we introduce a
118+
`JniObjectReference` struct type. This represents a JNI Local, Global, or
119+
WeakGlobal object reference. The `JniObjectReference` struct also contains
120+
the *reference type* as `JniObjectReferenceType`, formerly `JniReferenceType`.
121+
`jmethodID` and `jfieldID` become "normal" class types, permitting type safety,
122+
but lose their `SafeHandle` status, which was never really necessary because
123+
they don't require cleanup *anyway*. Furthermore, these values should be
124+
*cached* -- see `JniPeerMembers` -- so making them GC objects shouldn't be
125+
a long-term problem.
126+
127+
By doing so, we allow Java.Interop to have *two separate implementations*,
128+
controlled by build-time `#define`s:
129+
130+
* `FEATURE_HANDLES_ARE_SAFE_HANDLES`: Causes `JniObjectReference` to
131+
contain a `SafeHandle` wrapping the underlying JNI handle.
132+
* `FEATURE_HANDLES_ARE_INTPTRS`: Causes `JniObjectReference` to contain
133+
an `IntPtr` for the underlying JNI handle.
134+
135+
The rationale for this is twofold:
136+
137+
1. It allows swapping out "safer" `SafeHandle` and "less safe" `IntPtr`
138+
implementations, permitting easier performance comparisons.
139+
2. It allows migrating the existing code, as some of the existing
140+
tests may assume that JNI handles are garbage collected, which
141+
won't be the case when `FEATURE_HANDLES_ARE_INTPTRS` is set.
142+
143+
`FEATURE_HANDLES_ARE_INTPTRS` support is still in-progresss.
144+
79145
## Naming Conventions
80146

81147
Types with a `Java` prefix are "high-level" types which participate in cross-VM

samples/Hello/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ public static unsafe void Main (string[] args)
2121
Console.WriteLine ("Part 2!");
2222
using (var vm = new JreVMBuilder ().CreateJreVM ()) {
2323
Console.WriteLine ("# JniEnvironment.Current={0}", JniEnvironment.Current);
24-
Console.WriteLine ("vm.SafeHandle={0}", vm.SafeHandle);
24+
Console.WriteLine ("vm.SafeHandle={0}", vm.InvocationPointer);
2525
var t = new JniType ("java/lang/Object");
2626
var c = t.GetConstructor ("()V");
2727
var o = t.NewObject (c, null);
2828
var m = t.GetInstanceMethod ("hashCode", "()I");
2929
int i = m.CallVirtualInt32Method (o);
3030
Console.WriteLine ("java.lang.Object={0}", o);
3131
Console.WriteLine ("hashcode={0}", i);
32-
o.Dispose ();
32+
JniEnvironment.Handles.Dispose (ref o);
3333
t.Dispose ();
3434
// var o = JniTypes.FindClass ("java/lang/Object");
3535
/*

src/Android.Interop/Java.Interop/AndroidVM.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@
66
namespace Java.Interop {
77

88
// FOR TEST PURPOSES ONLY
9-
public delegate JniLocalReference SafeHandleDelegate_CallObjectMethodA (JniEnvironmentSafeHandle env, JniReferenceSafeHandle instance, JniInstanceMethodID method, JValue[] args);
10-
public delegate void SafeHandleDelegate_DeleteLocalRef (JniEnvironmentSafeHandle env, IntPtr handle);
9+
public delegate IntPtr SafeHandleDelegate_CallObjectMethodA (IntPtr env, IntPtr instance, IntPtr method, JValue[] args);
10+
public delegate void SafeHandleDelegate_DeleteLocalRef (IntPtr env, IntPtr handle);
1111

1212
class AndroidVMBuilder : JavaVMOptions {
1313

1414
public AndroidVMBuilder ()
1515
{
16-
EnvironmentHandle = new JniEnvironmentSafeHandle (JNIEnv.Handle);
16+
EnvironmentPointer = JNIEnv.Handle;
1717
NewObjectRequired = ((int) Android.OS.Build.VERSION.SdkInt) <= 10;
1818
using (var env = new JniEnvironment (JNIEnv.Handle)) {
19-
JavaVMSafeHandle vm;
19+
IntPtr vm;
2020
int r = JniEnvironment.Handles.GetJavaVM (out vm);
2121
if (r < 0)
2222
throw new InvalidOperationException ("JNIEnv::GetJavaVM() returned: " + r);
23-
VMHandle = vm;
23+
InvocationPointer = vm;
2424
}
2525
JniHandleManager = Java.InteropTests.LoggingJniHandleManagerDecorator.GetHandleManager (new JniHandleManager ());
2626
}
@@ -44,18 +44,16 @@ internal AndroidVM (AndroidVMBuilder builder)
4444
get {return current;}
4545
}
4646

47-
protected override bool TryGC (IJavaObject value, ref JniReferenceSafeHandle handle)
47+
protected override bool TryGC (IJavaObject value, ref JniObjectReference handle)
4848
{
49-
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC");
50-
if (handle == null || handle.IsInvalid)
49+
if (!handle.IsValid)
5150
return true;
5251
var wgref = handle.NewWeakGlobalRef ();
53-
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC: wgref=0x{0}", wgref.DangerousGetHandle().ToString ("x"));;
54-
handle.Dispose ();
52+
JniEnvironment.Handles.Dispose (ref handle);
5553
Java.Lang.Runtime.GetRuntime ().Gc ();
5654
handle = wgref.NewGlobalRef ();
57-
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC: handle.IsInvalid={0}", handle.IsInvalid);
58-
return handle == null || handle.IsInvalid;
55+
JniEnvironment.Handles.Dispose (ref wgref);
56+
return handle.IsValid;
5957
}
6058

6159
Dictionary<string, Type> typeMappings = new Dictionary<string, Type> ();

src/Android.Interop/Tests/TestsSample.cs

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,19 @@ TimeSpan GetXAMethodCallTiming ()
4848

4949
unsafe TimeSpan GetJIMethodCallTiming ()
5050
{
51-
using (var k = new JniType ("java/lang/Object"))
52-
using (var c = k.GetConstructor ("()V"))
53-
using (var o = k.NewObject (c, null))
54-
using (var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;")) {
51+
using (var k = new JniType ("java/lang/Object")) {
52+
var c = k.GetConstructor ("()V");
53+
var o = k.NewObject (c, null);
54+
var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;");
5555

5656
var sw = Stopwatch.StartNew ();
5757
for (int i = 0; i < Unified_ToString_Iterations; ++i) {
58-
using (var r = t.CallVirtualObjectMethod (o)) {
59-
}
58+
var r = t.CallVirtualObjectMethod (o);
59+
JniEnvironment.Handles.Dispose (ref r);
6060
}
6161
sw.Stop ();
62+
63+
JniEnvironment.Handles.Dispose (ref o);
6264
return sw.Elapsed;
6365
}
6466
}
@@ -98,15 +100,15 @@ unsafe void GetJICallObjectMethodAndDeleteLocalRefTimings (
98100
out TimeSpan unsafeCallObjectMethodTime, out TimeSpan unsafeDeleteLocalRefTime)
99101
{
100102

101-
using (var k = new JniType ("java/lang/Object"))
102-
using (var c = k.GetConstructor ("()V"))
103-
using (var o = k.NewObject (c, null))
104-
using (var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;")) {
103+
using (var k = new JniType ("java/lang/Object")) {
104+
var c = k.GetConstructor ("()V");
105+
var o = k.NewObject (c, null);
106+
var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;");
105107

106-
using (var r = t.CallVirtualObjectMethod (o)) {
107-
}
108+
var r = t.CallVirtualObjectMethod (o);
109+
JniEnvironment.Handles.Dispose (ref r);
108110

109-
var rs = new JniLocalReference [MaxLocalRefs];
111+
var rs = new JniObjectReference [MaxLocalRefs];
110112

111113
var sw = Stopwatch.StartNew ();
112114
for (int i = 0; i < rs.Length; ++i) {
@@ -117,7 +119,7 @@ unsafe void GetJICallObjectMethodAndDeleteLocalRefTimings (
117119

118120
sw.Restart ();
119121
for (int i = 0; i < rs.Length; ++i) {
120-
rs [i].Dispose ();
122+
JniEnvironment.Handles.Dispose (ref rs [i]);
121123
}
122124
sw.Stop ();
123125
disposeTime = sw.Elapsed;
@@ -134,28 +136,27 @@ unsafe void GetJICallObjectMethodAndDeleteLocalRefTimings (
134136
var usafeDel = (IntPtrDelegate_DeleteLocalRef)
135137
Marshal.GetDelegateForFunctionPointer (JNIEnv_DeleteLocalRef, typeof (IntPtrDelegate_DeleteLocalRef));
136138

137-
var sh = JniEnvironment.Current.SafeHandle;
138-
var uh = sh.DangerousGetHandle ();
139+
var uh = JniEnvironment.Current.EnvironmentPointer;
139140
var args = new JValue [0];
140141

141142
sw.Restart ();
142143
for (int i = 0; i < rs.Length; ++i) {
143-
rs [i] = safeCall (sh, o, t, args);
144+
rs [i] = new JniObjectReference (safeCall (uh, o.Handle, t.ID, args), JniObjectReferenceType.Local);
144145
}
145146
sw.Stop ();
146147
safeCallObjectMethodTime = sw.Elapsed;
147148

148149
sw.Restart ();
149150
for (int i = 0; i < rs.Length; ++i) {
150-
safeDel (sh, rs [i].DangerousGetHandle ());
151-
rs [i].SetHandleAsInvalid ();
151+
safeDel (uh, rs [i].Handle);
152+
rs [i] = new JniObjectReference ();
152153
}
153154
sw.Stop ();
154155
safeDeleteLocalRefTime = sw.Elapsed;
155156

156157
var urs = new IntPtr [MaxLocalRefs];
157-
var ut = t.DangerousGetHandle ();
158-
var uo = o.DangerousGetHandle ();
158+
var ut = t.ID;
159+
var uo = o.Handle;
159160

160161
sw.Restart ();
161162
for (int i = 0; i < urs.Length; ++i) {

src/Java.Interop.Dynamic/Java.Interop.Dynamic/DynamicJavaClass.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ protected override bool Disposed {
8585
get {return klass.disposed;}
8686
}
8787

88-
protected override JniReferenceSafeHandle ConversionTarget {
88+
protected override JniObjectReference ConversionTarget {
8989
get {
90-
return klass.info.Members.JniPeerType.SafeHandle;
90+
return klass.info.Members.JniPeerType.PeerReference;
9191
}
9292
}
9393

@@ -139,15 +139,15 @@ static class JavaModifiers {
139139
static JavaModifiers ()
140140
{
141141
using (var t = new JniType ("java/lang/reflect/Modifier")) {
142-
using (var s = t.GetStaticField ("STATIC", "I"))
143-
Static = s.GetInt32Value (t.SafeHandle);
142+
var s = t.GetStaticField ("STATIC", "I");
143+
Static = s.GetInt32Value (t.PeerReference);
144144
}
145145
}
146146
}
147147

148148
struct JniArgumentMarshalInfo {
149149
JValue jvalue;
150-
JniLocalReference lref;
150+
JniObjectReference lref;
151151
IJavaObject obj;
152152
Action<IJavaObject, object> cleanup;
153153

@@ -177,8 +177,7 @@ public void Cleanup (object value)
177177
{
178178
if (cleanup != null && obj != null)
179179
cleanup (obj, value);
180-
if (lref != null)
181-
lref.Dispose ();
180+
JniEnvironment.Handles.Dispose (ref lref);
182181
}
183182
}
184183
}

src/Java.Interop.Dynamic/Java.Interop.Dynamic/DynamicJavaInstance.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public DynamicJavaInstance (IJavaObject value)
2525

2626
Value = value;
2727

28-
var type = JniEnvironment.Types.GetJniTypeNameFromInstance (value.SafeHandle);
28+
var type = JniEnvironment.Types.GetJniTypeNameFromInstance (value.PeerReference);
2929
klass = JavaClassInfo.GetClassInfo (type);
3030
}
3131

@@ -78,9 +78,9 @@ protected override bool Disposed {
7878
get {return instance.disposed;}
7979
}
8080

81-
protected override JniReferenceSafeHandle ConversionTarget {
81+
protected override JniObjectReference ConversionTarget {
8282
get {
83-
return instance.Value.SafeHandle;
83+
return instance.Value.PeerReference;
8484
}
8585
}
8686

0 commit comments

Comments
 (0)