Skip to content

Commit

Permalink
[Java.Interop] Expunge SafeHandles from the public API.
Browse files Browse the repository at this point in the history
(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.
  • Loading branch information
jonpryor committed Oct 22, 2015
1 parent ce03a52 commit 25de1f3
Show file tree
Hide file tree
Showing 94 changed files with 22,111 additions and 1,808 deletions.
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ If you fail to re-install the runtime package, then `jar` will fail to run:

## Type Safety

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

Unfortunately, various tests demonstrated that while `SafeHandle`s provided
increased type safety, they did so at a large runtime cost:

1. `SafeHandle`s are reference types, increasing GC heap allocations and pressure.
2. [`SafeHandle`s are *thread-safe* in order to prevent race conditions and handle recycling attacks][reliability].

[reliability]: http://blogs.msdn.com/b/bclteam/archive/2005/03/16/396900.aspx

Compared to a Xamarin.Android-like "use `IntPtr`s for *everything*" binding
approach, the overhread is significant: to *just* invoke
`JNIEnv::CallObjectMethod()`, using `SafeHandle`s for everything causes
execution time to take ~1.4x longer than a comparable struct-oriented approach.

Make the test more realistic -- compared to current Xamarin.Android and
current Java.Interop -- so that `JniEnvironment.Members.CallObjectMethod()`
also calls `JniEnvironment.Errors.ExceptionOccurred()`, which also returns
a JNI local reference -- and runtime execution time *jumped to ~3.6x*:

# SafeHandle timing: 00:00:09.9393493
# Average Invocation: 0.00099393493ms
# JniObjectReference timing: 00:00:02.7254572
# Average Invocation: 0.00027254572ms

(See the [tests/invocation-overhead](tests/invocation-overhead) directory
for the invocation comparison sourcecode.)

*This is not acceptable*. Performance is a known issue with Xamarin.Android;
we can't be making it *worse*.

Meanwhile, I *really* dislike using `IntPtr`s everywhere, as it doesn't let you
know what the value actually represents.

To solve this issue, *avoid `SafeHandle` types* in the public API.

Downside: this means we can't have the GC collect our garbage JNI references.

Upside: the Java.Interop effort will actually be usable.

Instead of using `SafeHandle` types, we introduce a
`JniObjectReference` struct type. This represents a JNI Local, Global, or
WeakGlobal object reference. The `JniObjectReference` struct also contains
the *reference type* as `JniObjectReferenceType`, formerly `JniReferenceType`.
`jmethodID` and `jfieldID` become "normal" class types, permitting type safety,
but lose their `SafeHandle` status, which was never really necessary because
they don't require cleanup *anyway*. Furthermore, these values should be
*cached* -- see `JniPeerMembers` -- so making them GC objects shouldn't be
a long-term problem.

By doing so, we allow Java.Interop to have *two separate implementations*,
controlled by build-time `#define`s:

* `FEATURE_HANDLES_ARE_SAFE_HANDLES`: Causes `JniObjectReference` to
contain a `SafeHandle` wrapping the underlying JNI handle.
* `FEATURE_HANDLES_ARE_INTPTRS`: Causes `JniObjectReference` to contain
an `IntPtr` for the underlying JNI handle.

The rationale for this is twofold:

1. It allows swapping out "safer" `SafeHandle` and "less safe" `IntPtr`
implementations, permitting easier performance comparisons.
2. It allows migrating the existing code, as some of the existing
tests may assume that JNI handles are garbage collected, which
won't be the case when `FEATURE_HANDLES_ARE_INTPTRS` is set.

`FEATURE_HANDLES_ARE_INTPTRS` support is still in-progresss.

## Naming Conventions

Types with a `Java` prefix are "high-level" types which participate in cross-VM
Expand Down
4 changes: 2 additions & 2 deletions samples/Hello/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ public static unsafe void Main (string[] args)
Console.WriteLine ("Part 2!");
using (var vm = new JreVMBuilder ().CreateJreVM ()) {
Console.WriteLine ("# JniEnvironment.Current={0}", JniEnvironment.Current);
Console.WriteLine ("vm.SafeHandle={0}", vm.SafeHandle);
Console.WriteLine ("vm.SafeHandle={0}", vm.InvocationPointer);
var t = new JniType ("java/lang/Object");
var c = t.GetConstructor ("()V");
var o = t.NewObject (c, null);
var m = t.GetInstanceMethod ("hashCode", "()I");
int i = m.CallVirtualInt32Method (o);
Console.WriteLine ("java.lang.Object={0}", o);
Console.WriteLine ("hashcode={0}", i);
o.Dispose ();
JniEnvironment.Handles.Dispose (ref o);
t.Dispose ();
// var o = JniTypes.FindClass ("java/lang/Object");
/*
Expand Down
22 changes: 10 additions & 12 deletions src/Android.Interop/Java.Interop/AndroidVM.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@
namespace Java.Interop {

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

class AndroidVMBuilder : JavaVMOptions {

public AndroidVMBuilder ()
{
EnvironmentHandle = new JniEnvironmentSafeHandle (JNIEnv.Handle);
EnvironmentPointer = JNIEnv.Handle;
NewObjectRequired = ((int) Android.OS.Build.VERSION.SdkInt) <= 10;
using (var env = new JniEnvironment (JNIEnv.Handle)) {
JavaVMSafeHandle vm;
IntPtr vm;
int r = JniEnvironment.Handles.GetJavaVM (out vm);
if (r < 0)
throw new InvalidOperationException ("JNIEnv::GetJavaVM() returned: " + r);
VMHandle = vm;
InvocationPointer = vm;
}
JniHandleManager = Java.InteropTests.LoggingJniHandleManagerDecorator.GetHandleManager (new JniHandleManager ());
}
Expand All @@ -44,18 +44,16 @@ internal AndroidVM (AndroidVMBuilder builder)
get {return current;}
}

protected override bool TryGC (IJavaObject value, ref JniReferenceSafeHandle handle)
protected override bool TryGC (IJavaObject value, ref JniObjectReference handle)
{
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC");
if (handle == null || handle.IsInvalid)
if (!handle.IsValid)
return true;
var wgref = handle.NewWeakGlobalRef ();
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC: wgref=0x{0}", wgref.DangerousGetHandle().ToString ("x"));;
handle.Dispose ();
JniEnvironment.Handles.Dispose (ref handle);
Java.Lang.Runtime.GetRuntime ().Gc ();
handle = wgref.NewGlobalRef ();
System.Diagnostics.Debug.WriteLine ("# AndroidVM.TryGC: handle.IsInvalid={0}", handle.IsInvalid);
return handle == null || handle.IsInvalid;
JniEnvironment.Handles.Dispose (ref wgref);
return handle.IsValid;
}

Dictionary<string, Type> typeMappings = new Dictionary<string, Type> ();
Expand Down
43 changes: 22 additions & 21 deletions src/Android.Interop/Tests/TestsSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,19 @@ TimeSpan GetXAMethodCallTiming ()

unsafe TimeSpan GetJIMethodCallTiming ()
{
using (var k = new JniType ("java/lang/Object"))
using (var c = k.GetConstructor ("()V"))
using (var o = k.NewObject (c, null))
using (var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;")) {
using (var k = new JniType ("java/lang/Object")) {
var c = k.GetConstructor ("()V");
var o = k.NewObject (c, null);
var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;");

var sw = Stopwatch.StartNew ();
for (int i = 0; i < Unified_ToString_Iterations; ++i) {
using (var r = t.CallVirtualObjectMethod (o)) {
}
var r = t.CallVirtualObjectMethod (o);
JniEnvironment.Handles.Dispose (ref r);
}
sw.Stop ();

JniEnvironment.Handles.Dispose (ref o);
return sw.Elapsed;
}
}
Expand Down Expand Up @@ -98,15 +100,15 @@ unsafe void GetJICallObjectMethodAndDeleteLocalRefTimings (
out TimeSpan unsafeCallObjectMethodTime, out TimeSpan unsafeDeleteLocalRefTime)
{

using (var k = new JniType ("java/lang/Object"))
using (var c = k.GetConstructor ("()V"))
using (var o = k.NewObject (c, null))
using (var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;")) {
using (var k = new JniType ("java/lang/Object")) {
var c = k.GetConstructor ("()V");
var o = k.NewObject (c, null);
var t = k.GetInstanceMethod ("toString", "()Ljava/lang/String;");

using (var r = t.CallVirtualObjectMethod (o)) {
}
var r = t.CallVirtualObjectMethod (o);
JniEnvironment.Handles.Dispose (ref r);

var rs = new JniLocalReference [MaxLocalRefs];
var rs = new JniObjectReference [MaxLocalRefs];

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

sw.Restart ();
for (int i = 0; i < rs.Length; ++i) {
rs [i].Dispose ();
JniEnvironment.Handles.Dispose (ref rs [i]);
}
sw.Stop ();
disposeTime = sw.Elapsed;
Expand All @@ -134,28 +136,27 @@ unsafe void GetJICallObjectMethodAndDeleteLocalRefTimings (
var usafeDel = (IntPtrDelegate_DeleteLocalRef)
Marshal.GetDelegateForFunctionPointer (JNIEnv_DeleteLocalRef, typeof (IntPtrDelegate_DeleteLocalRef));

var sh = JniEnvironment.Current.SafeHandle;
var uh = sh.DangerousGetHandle ();
var uh = JniEnvironment.Current.EnvironmentPointer;
var args = new JValue [0];

sw.Restart ();
for (int i = 0; i < rs.Length; ++i) {
rs [i] = safeCall (sh, o, t, args);
rs [i] = new JniObjectReference (safeCall (uh, o.Handle, t.ID, args), JniObjectReferenceType.Local);
}
sw.Stop ();
safeCallObjectMethodTime = sw.Elapsed;

sw.Restart ();
for (int i = 0; i < rs.Length; ++i) {
safeDel (sh, rs [i].DangerousGetHandle ());
rs [i].SetHandleAsInvalid ();
safeDel (uh, rs [i].Handle);
rs [i] = new JniObjectReference ();
}
sw.Stop ();
safeDeleteLocalRefTime = sw.Elapsed;

var urs = new IntPtr [MaxLocalRefs];
var ut = t.DangerousGetHandle ();
var uo = o.DangerousGetHandle ();
var ut = t.ID;
var uo = o.Handle;

sw.Restart ();
for (int i = 0; i < urs.Length; ++i) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ protected override bool Disposed {
get {return klass.disposed;}
}

protected override JniReferenceSafeHandle ConversionTarget {
protected override JniObjectReference ConversionTarget {
get {
return klass.info.Members.JniPeerType.SafeHandle;
return klass.info.Members.JniPeerType.PeerReference;
}
}

Expand Down Expand Up @@ -139,15 +139,15 @@ static class JavaModifiers {
static JavaModifiers ()
{
using (var t = new JniType ("java/lang/reflect/Modifier")) {
using (var s = t.GetStaticField ("STATIC", "I"))
Static = s.GetInt32Value (t.SafeHandle);
var s = t.GetStaticField ("STATIC", "I");
Static = s.GetInt32Value (t.PeerReference);
}
}
}

struct JniArgumentMarshalInfo {
JValue jvalue;
JniLocalReference lref;
JniObjectReference lref;
IJavaObject obj;
Action<IJavaObject, object> cleanup;

Expand Down Expand Up @@ -177,8 +177,7 @@ public void Cleanup (object value)
{
if (cleanup != null && obj != null)
cleanup (obj, value);
if (lref != null)
lref.Dispose ();
JniEnvironment.Handles.Dispose (ref lref);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public DynamicJavaInstance (IJavaObject value)

Value = value;

var type = JniEnvironment.Types.GetJniTypeNameFromInstance (value.SafeHandle);
var type = JniEnvironment.Types.GetJniTypeNameFromInstance (value.PeerReference);
klass = JavaClassInfo.GetClassInfo (type);
}

Expand Down Expand Up @@ -78,9 +78,9 @@ protected override bool Disposed {
get {return instance.disposed;}
}

protected override JniReferenceSafeHandle ConversionTarget {
protected override JniObjectReference ConversionTarget {
get {
return instance.Value.SafeHandle;
return instance.Value.PeerReference;
}
}

Expand Down
Loading

0 comments on commit 25de1f3

Please sign in to comment.