Skip to content

Commit

Permalink
[Java.Interop.Dynamic] Support field lookup fallback.
Browse files Browse the repository at this point in the history
System.Dynamic.DynamicMetaObject has a concept of "fallback"
metaobjects: if the DynamicMetaObject can't perform a particular
operation, the "host site" can be called upon to perform the
operation instead. See §5.4.3.1 in:

	https://www.codeplex.com/Download?ProjectName=dlr&DownloadId=127512

For example, consider DynamicJavaClass.JniClassName: this is a public
member (which perhaps shouldn't be?) which contains the JNI class name
provided to the constructor:

	partial class DynamicJavaClass {
		public DynamicJavaClass (string jniClassName);

		public string JniClassName {get;}
	}

It is thus "obvious" to want to access this member:

	var d = new DynamicJavaClass ("foo/Bar");
	// d.JniClassName == "foo/Bar"

Accessing such "real" members requires using the fallback mechanism;
without it, attempting to access DynamicJavaClass.JniClassName results
in a runtime exception from JniPeerStaticFields.GetValue()[0].

	dynamic d = new DynamicJavaClass ("foo/Bar");
	string  s = d.JniClassName;
	// BOOM [0]

The cause of the crash is a bug (lol): JniTypeInfo.ToString() was
used, not JniTypeInfo.JniTypeReference. The latter always uses `L...;`
for reference types; the former does not, and the latter is required
for JNIEnv::GetStaticFieldID(). Since `java/lang/String` isn't in the
correct format, it threw.

Fixing this results in a different, expected, exception [1]. Because
the Java type doesn't contain a JniClassName field,
JNIEnv::GetStaticFieldID() raises an exception, stopping all progress.

The fix is to take a page out of DynamicObject:

	https://msdn.microsoft.com/en-us/library/system.dynamic.dynamicobject(v=vs.110).aspx
	https://github.com/Microsoft/referencesource/blob/9da503f9ef21e8d1f2905c78d4e3e5cbb3d6f85a/System.Core/Microsoft/Scripting/Actions/DynamicObject.cs

Instead of immediately performing the operation, rename
DynamicJavaClass.GetStaticFieldValue() to
DynamicJavaClass.TryGetStaticMemberValue(), which (1) returns `bool`,
and (2) uses an `out` parameter to store the value.
DynamicJavaClass.TryGetStaticMemberValue() internally catches the
JavaException, and returns whether or not the lookup was successful.

Consider:

	dynamic d = new DynamicJavaClass ("java/lang/Object");
	string  s = d.JniClassName;

Instead of converting this into:

	string s = (string) d.GetStaticFieldValue ("JniClassName", typeof (string));

we instead generate:

	object __v;
	string s = d.TryGetStaticMemberValue ("JniClassName", typeof (string), out __v)
		? (string) __v
		: (string) [[ Insert GetMemberBinder.FallbackGetMember() behavior here ]]

which amounts to:

	object __v;
	string s = d.TryGetStaticMemberValue ("JniClassName", typeof (string), out __v)
		? (string) __v
		: (string) d.JniClassName;

This works as expected, and allows JNI lookup to be performed first,
and when it fails to fallback on e.g. C# reflection.

There is one flaw, however: we still require a conversion step.
"Explicit" member access works:

	dynamic d = new DynamicJavaClass ("java/lang/Object");
	string  s = d.JniClassName;
	// Now works

"Implicit" member access fails:

	dynamic d = new DynamicJavaClass ("java/lang/Object");
	Assert.AreEqual ("java/lang/Object", d.JniClassName);
	// BOOM [2]

The cause for this is that "GetMember" returns an intermediate
DeferredConvert<T> instance instead of the member itself, because we
need to know the final type to perform JNI lookup. When there is no
"convert to the actual type" step, as above, we're left with the
intermediate, and things blow up.

I can't think of a good fix to this, though a "workaround" is to
provide the conversion, e.g. through a cast:

	dynamic d = new DynamicJavaClass ("java/lang/Object");
	Assert.AreEqual ("java/lang/Object", (string) d.JniClassName);
	// Works.

Aside: We change the default BindingRestrictions for
DynamicMetaObject<T> to (hopefully?) better support call-site caching.
I'm still not sure how to even TEST this, much less "properly" fix it
(is it broken?), but I *think* this would be "more correct."

[0]: System.NotSupportedException : Unsupported argument type: java/lang/String
   at Java.Interop.JniPeerStaticFields.GetValue (string) [0x0012d] in .../src/Java.Interop/Java.Interop/JniPeerStaticFields.cs:49
   at Java.Interop.Dynamic.DynamicJavaClass.GetStaticFieldValue (string,System.Type) [0x00058] in ...k/src/Java.Interop.Dynamic/Java.Interop.Dynamic/DynamicJavaClass.cs:88
   at (wrapper dynamic-method) object.CallSite.Target (System.Runtime.CompilerServices.Closure,System.Runtime.CompilerServices.CallSite,object)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1<object, string> (System.Runtime.CompilerServices.CallSite,object)
   at Java.Interop.DynamicTests.DynamicJavaClassTests.JniClassName () [0x0000c] in .../src/Java.Interop.Dynamic/Tests/Java.Interop.Dynamic/DynamicJavaClassTests.cs:29
   at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
   at System.Reflection.MonoMethod.Invoke (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo) [0x00054] in /private/tmp/source-mono-mac-4.0.0-branch-c5sr4/bockbuild-mono-4.0.0-branch/profiles/mono-mac-xamarin/build-root/mono-4.0.4/mcs/class/corlib/System.Reflection/MonoMethod.cs:230

[1]: Java.Interop.JavaException : JniClassName
   at Java.Interop.JniEnvironment/Members.GetStaticFieldID (Java.Interop.JniReferenceSafeHandle,string,string) [0x00086] in .../src/Java.Interop/Java.Interop/JniEnvironment.g.cs:2612
   at Java.Interop.JniType.GetStaticField (string,string) [0x0000f] in .../src/Java.Interop/Java.Interop/JniType.cs:212
   at Java.Interop.JniPeerStaticFields.GetFieldID (string) [0x0003f] in .../src/Java.Interop/Java.Interop/JniPeerStaticFields.cs:23
   at Java.Interop.JniPeerStaticFields.GetObjectValue (string) [0x00003] in .../src/Java.Interop/Java.Interop/JniPeerFields.cs:268
   at Java.Interop.JniPeerStaticFields.GetValue (string) [0x000f6] in .../src/Java.Interop/Java.Interop/JniPeerStaticFields.cs:44
   at Java.Interop.Dynamic.DynamicJavaClass.GetStaticFieldValue (string,System.Type) [0x00052] in .../src/Java.Interop.Dynamic/Java.Interop.Dynamic/DynamicJavaClass.cs:88
   at (wrapper dynamic-method) object.CallSite.Target (System.Runtime.CompilerServices.Closure,System.Runtime.CompilerServices.CallSite,object)
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1<object, string> (System.Runtime.CompilerServices.CallSite,object)
   at Java.Interop.DynamicTests.DynamicJavaClassTests.JniClassName () [0x0000c] in .../src/Java.Interop.Dynamic/Tests/Java.Interop.Dynamic/DynamicJavaClassTests.cs:29
   at (wrapper managed-to-native) System.Reflection.MonoMethod.InternalInvoke (System.Reflection.MonoMethod,object,object[],System.Exception&)
   at System.Reflection.MonoMethod.Invoke (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo) [0x00054] in /private/tmp/source-mono-mac-4.0.0-branch-c5sr4/bockbuild-mono-4.0.0-branch/profiles/mono-mac-xamarin/build-root/mono-4.0.4/mcs/class/corlib/System.Reflection/MonoMethod.cs:230

     --- End of managed Java.Interop.JavaException stack trace ---
   java.lang.NoSuchFieldError: JniClassName

[2]: Test Failure : Java.Interop.DynamicTests.DynamicJavaClassTests.JniClassName
 Expected: "java/util/Arrays"
  But was:  <Java.Interop.Dynamic.DeferredConvert`1[Java.Interop.Dynamic.DynamicJavaClass]>
  • Loading branch information
jonpryor committed Sep 14, 2015
1 parent 3b1a91a commit f62858a
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 49 deletions.
4 changes: 4 additions & 0 deletions src/Java.Interop.Dynamic/Java.Interop.Dynamic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@
<Project>{94BD81F7-B06F-4295-9636-F8A3B6BDC762}</Project>
<Name>Java.Interop</Name>
</ProjectReference>
<ProjectReference Include="..\..\lib\mono.linq.expressions\Mono.Linq.Expressions.csproj">
<Project>{0C001D50-4176-45AE-BDC8-BA626508B0CC}</Project>
<Name>Mono.Linq.Expressions</Name>
</ProjectReference>
</ItemGroup>
</Project>
98 changes: 52 additions & 46 deletions src/Java.Interop.Dynamic/Java.Interop.Dynamic/DynamicJavaClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

using Java.Interop;

using Mono.Linq.Expressions;

namespace Java.Interop.Dynamic {

public class DynamicJavaClass : IDynamicMetaObjectProvider
Expand Down Expand Up @@ -75,32 +77,35 @@ static string GetEncodedJniSignature (InvokeMemberBinder binder, DynamicMetaObje
sb.Append (typeInfo.ToString ());
}
sb.Append (")");
sb.Append (JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (returnType).ToString ());
sb.Append (JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (returnType).JniTypeReference);

return sb.ToString ();
}

public object GetStaticFieldValue (string fieldName, Type fieldType)
internal bool TryGetStaticMemberValue (string fieldName, Type fieldType, out object value)
{
Debug.WriteLine ("# DynamicJavaClass({0}).field({1}) as {2}", JniClassName, fieldName, fieldType);
var typeInfo = JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (fieldType);
var encoded = fieldName + "\u0000" + typeInfo.ToString ();
return members.StaticFields.GetValue (encoded);
var encoded = fieldName + "\u0000" + typeInfo.JniTypeReference;
try {
value = members.StaticFields.GetValue (encoded);
return true;
}
catch (JavaException e) {
value = null;
e.Dispose ();
return false;
}
}

public void SetStaticFieldValue (string fieldName, Type fieldType, object value)
{
Debug.WriteLine ("# DynamicJavaClass({0}).field({1}) as {2} = {3}", JniClassName, fieldName, fieldType, value);
var typeInfo = JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (fieldType);
var encoded = fieldName + "\u0000" + typeInfo.ToString ();
var encoded = fieldName + "\u0000" + typeInfo.JniTypeReference;
members.StaticFields.SetValue (encoded, value);
}

internal StaticFieldAccess GetStaticFieldAccess (string fieldName)
{
return new StaticFieldAccess (this, fieldName);
}

#if false
Type CreateManagedPeerType ()
{
Expand Down Expand Up @@ -131,13 +136,15 @@ public Expression ExpressionAsT {
}

public DynamicMetaObject (Expression parameter, T value)
: base (parameter, BindingRestrictions.Empty, value)
: base (parameter, BindingRestrictions.GetInstanceRestriction (parameter, value), value)
{
}
}

class MetaStaticMemberAccessObject : DynamicMetaObject<DynamicJavaClass>
{
delegate bool TryGetStaticMemberValue (string fieldName, Type fieldType, out object value);

public MetaStaticMemberAccessObject (Expression parameter, DynamicJavaClass value)
: base (parameter, value)
{
Expand Down Expand Up @@ -196,16 +203,15 @@ public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicM
public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
{
// Console.WriteLine ("GetMember: Expression={0} [{1}]; property={2}", Expression.ToCSharpCode (), Expression.Type, binder.Name);
var expr =
Expression.Call (
ExpressionAsT,
typeof (DynamicJavaClass).GetMethod ("GetStaticFieldAccess", BindingFlags.Instance | BindingFlags.NonPublic),
Expression.Constant (binder.Name));
return new DynamicMetaObject (
expr,
BindingRestrictions.GetTypeRestriction (
expr,
typeof (StaticFieldAccess)));
TryGetStaticMemberValue m = Value.TryGetStaticMemberValue;
var access = new DeferredConvert<DynamicJavaClass> {
Arguments = new[]{Expression.Constant (binder.Name)},
Instance = Value,
FallbackCreator = binder.FallbackGetMember,
Method = m.Method,
};
var accessE = Expression.Constant (access);
return new DynamicMetaObject (accessE, BindingRestrictions.GetInstanceRestriction (accessE, access));
}
}

Expand Down Expand Up @@ -257,45 +263,45 @@ public override DynamicMetaObject BindConvert (ConvertBinder binder)
}
}

class StaticFieldAccess : IDynamicMetaObjectProvider {
class DeferredConvert<T> : IDynamicMetaObjectProvider {

public string FieldName {get; private set;}
public DynamicJavaClass JavaClass {get; private set;}

public StaticFieldAccess (DynamicJavaClass klass, string fieldName)
{
JavaClass = klass;
FieldName = fieldName;
}
public T Instance;
public MethodInfo Method;
public Expression[] Arguments;
public Func<DynamicMetaObject, DynamicMetaObject> FallbackCreator;

public DynamicMetaObject GetMetaObject(Expression parameter)
public DynamicMetaObject GetMetaObject (Expression parameter)
{
// Console.WriteLine ("# FieldAccessInfo.GetMetaObject: parameter={0}", parameter);
return new MetaStaticFieldAccessObject(parameter, this);
return new DeferredConvertMetaObject<T> (parameter, this);
}
}

class MetaStaticFieldAccessObject : DynamicMetaObject<StaticFieldAccess> {
class DeferredConvertMetaObject<T> : DynamicMetaObject<DeferredConvert<T>> {

public MetaStaticFieldAccessObject (Expression e, StaticFieldAccess value)
public DeferredConvertMetaObject (Expression e, DeferredConvert<T> value)
: base (e, value)
{
// Console.WriteLine ("MyHelperObject: e={0}", e.ToCSharpCode ());
Debug.WriteLine ("DeferredConvertMetaObject<{0}>: e={1}", typeof (T).Name, e.ToCSharpCode ());
}

public override DynamicMetaObject BindConvert (ConvertBinder binder)
{
// Console.WriteLine ("MetaStaticFieldAccessObject.Convert: Expression={0} [{1}]", Expression.ToCSharpCode (), Expression.Type);

var field = ExpressionAsT;
var instance = Expression.Property (field, "JavaClass");
var fieldName = Expression.Property (field, "FieldName");
var gsfv = typeof(DynamicJavaClass).GetMethod ("GetStaticFieldValue");
var expr = Expression.Convert (
Expression.Call (instance, gsfv, fieldName, Expression.Constant (binder.Type)),
binder.Type);
Debug.WriteLine ("DeferredConvertMetaObject<{0}>.BindConvert: Expression='{1}'; Expression.Type={2}", typeof (T).Name, Expression.ToCSharpCode (), Expression.Type);

var instance = Expression.Constant (Value.Instance);
var instanceMO = new DynamicMetaObject (instance, BindingRestrictions.GetInstanceRestriction (instance, typeof (T)));
var value = Expression.Parameter (typeof (object), "value");
Debug.WriteLine ("DeferredConvertMetaObject<{0}>.BindConvert: Fallback={1}", typeof (T).Name, Value.FallbackCreator (instanceMO).Expression.ToCSharpCode ());
var call = Expression.Block (
new[]{value},
Expression.Condition (
test: Expression.Call (instance, Value.Method, Value.Arguments.Concat (new Expression[]{Expression.Constant (binder.Type), value})),
ifTrue: Expression.Convert (value, binder.Type),
ifFalse: Expression.Convert (Value.FallbackCreator (instanceMO).Expression, binder.Type))
);
Debug.WriteLine ("MetaStaticFieldAccessObject.Convert: call={0}", call.ToCSharpCode ());
return new DynamicMetaObject (
expr,
call,
BindingRestrictions.GetInstanceRestriction (Expression, Value));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,27 @@ namespace Java.Interop.DynamicTests {
[TestFixture]
class DynamicJavaClassTests : Java.InteropTests.JavaVMFixture
{
const string Arrays_class = "java/util/Arrays";
const string Integer_class = "java/lang/Integer";

[Test]
public void Constructor ()
{
Assert.Throws<ArgumentNullException> (() => new DynamicJavaClass (null));
}

[Test]
public void JniClassName ()
{
dynamic Arrays = new DynamicJavaClass (Arrays_class);
string name = Arrays.JniClassName;
Assert.AreEqual (Arrays_class, name);
}

[Test]
public void CallStaticMethod ()
{
dynamic Arrays = new DynamicJavaClass ("java/util/Arrays");
dynamic Arrays = new DynamicJavaClass (Arrays_class);
var array = new int[]{ 1, 2, 3, 4 };
int value = 3;
int index = Arrays.binarySearch (array, value);
Expand All @@ -35,15 +46,15 @@ public void CallStaticMethod ()
[Test]
public void ReadStaticMember ()
{
dynamic Integer = new DynamicJavaClass ("java/lang/Integer");
dynamic Integer = new DynamicJavaClass (Integer_class);
int max = Integer.MAX_VALUE;
Assert.AreEqual (int.MaxValue, max);
}

[Test]
public void WriteStaticMember ()
{
dynamic Integer = new DynamicJavaClass ("java/lang/Integer");
dynamic Integer = new DynamicJavaClass (Integer_class);
int cur = Integer.MAX_VALUE;
Console.WriteLine ("# MAX_VALUE={0}", cur);
Integer.MAX_VALUE = 42;
Expand Down

0 comments on commit f62858a

Please sign in to comment.