Skip to content

Commit 1c99956

Browse files
committed
[Java.Interop] Java-initiated instance activation
Fixes: #11 (Last seen nearly *two years ago* in commit 8c83f64...) An IJavaPeerable implementation has two halfs: the managed peer and the native peer (51f7862). The managed peer references the native peer via IJavaPeerable.PeerReference, and an association from the native peer to the managed peer is available through e.g. JniRuntime.JniValueManager.PeekPeer(JniObjectReference). Thus, the Very Important Question™: How is this association created? From constructors. Note the plural, and I don't mean constructor overloads. *Both* managed peer and user peer constructors are (mostly [0]) responsible for setting up the assocation. Managed peers do so via JniRuntime.JniValueManager.Construct() (b3a2162; formerly SetObjectPeerReference() in 25de1f3): /* 1 */ public unsafe BindingConstructor () /* 2 */ : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) /* 3 */ { /* 4 */ if (PeerReference.IsValid) /* 5 */ return; /* 6 */ /* 7 */ var peer = JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType (), null); /* 8 */ JniEnvironment.Runtime.ValueManager.Construct (this, ref peer, JniObjectReferenceOptions.CopyAndDispose); /* 9 */ JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this, null); /* 10 */ } Line 7 creates the native peer, line 8 calls JniRuntime.JniValueManager.Construct(), which calls IJavaPeerable.SetPeerReference() with a GREF and creates the association between `peer` and `this`, and line 9 finishes cleanup. (What about lines 2, 4, and 5? Those are new; see below.) Then we need "the other direction", when Java code performs the construction. This process of calling a managed peer constructor with a previously instantiated native peer instance is called *activation*. In 8c83f64, activation support through the Java ManagedPeer.runConstructor() API was stubbed and did nothing. Implement ManagedPeer.runConstructor() so that when a Java User peer is constructed, an instance of the associated managed peer is created. This turns out to be rather complicated, for all manner of reasons. First, the happy path: public MyJavaType (Args...) { com.xamarin.java_interop.ManagedPeer.runConstructor ( MyJavaType.class, this, "AssemblyQualifiedNameOfManagedPeer", "`:`-separated AssemblyQualified signature of AssemblyQualifiedNameOfManagedPeer constructor", args... } When Java code invokes `new MyJavaType(...)`, ManagedPeer.runConstructor() is executed (8c83f64), which will call ManagedPeer.RunConstructor(). ManagedPeer.RunConstructor() will find the specified managed peer type, lookup the specified constructor based on the signature, marshal the parameters, and invoke the specified constructor. Pause briefly to see the problem... The problem is that in this scenario, the Java instance is created *first*. Consequently, the "normal" managed peer constructor implementation of JniPeerMembers.InstanceMethods.StartCreateInstance() WON'T WORK, as that creats a NEW Java instance, not an existing one. Further complicating matters is that we want a "natural" experience, not a special constructor signature, so how do we provide the *existing* Java instance to the managed peer that will be created? The answer? By violating all manner of "invariants" by using Serialization-related APIs. Let's rephrase things: Given a Java type J and a Managed Peer type M, how do we associate an instance of J to an instance of M *before constructing M*? With FormatterServices.GetUninitializedObject(), which returns a blob of zero-initialized GC memory of "type" M which hasn't been constructed yet. There be potential dragons here. We then call IJavaPeerable.SetPeerReferenc() on this *unconstructed* instance, then use Reflection to invoke the constructor on the pre-allocated instance. Thus: object m = FormatterServices.GetUninitializedObject(typeof(M)); ((IJavaPeerable) o).SetPeerReference (J_instance); constructorInfoOfM.Invoke(m, args); (The stunning thing is that the above works at all!) This allows the Java peer instance to be associated with a Managed peer instance *before constructing the Managed peer*, which thus allows invoking the desired Managed peer constructor. Which brings us to the newly added lines 2, 4, and 5: /* 1 */ public unsafe BindingConstructor () /* 2 */ : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) /* 3 */ { /* 4 */ if (PeerReference.IsValid) /* 5 */ return; All "binding" constructors -- constructors which would normally use JniPeerMembers.InstanceMethods.StartCreateInstance() and JniRuntime.JniValueManager.Construct() -- must now call the JavaObject(JniObjectReference&, JniObjectReferenceOptions) or JavaException(JniObjectReference&, JniObjectReferenceOptions) constructors (line 2), to check for a "previously" set PeerReference value. If there was a previously set PeerReference value -- i.e. we're undergoing an activation-originated constructor call -- then the constructor should STOP EXECUTING (lines 4-5). Failure to do so will result in creating a new Java instance, and associating the current managed peer instance to the *wrong* Java instance. Don't Do That. What about the less-than-happy path? That's when a virtual method is invoked from a Java constructor and overridden in the managed peer. This is described somewhat in 972c5bc and 3043d89. The use of JNIEnv::AllocObject() (972c5bc) helps when the Managed peer is constructed first. When the native peer is constructed first, JNIEnv::AllocObject() can't help you. In this scenario, the Managed peer instance is instead constructed *multiple times* 1. Java: D.<init>() 2. Java: B.<init>() 3. Java: B.calledFromConstructor() 4. Java: D.n_calledFromConstructor(); 5. C#: D.CalledFromConstructorHandler() 6. C#: JniRuntime.JniValueManager.CreateProxy() for (1) This invokes the D(JniObjectReference&, JniObjectReferenceOptions) constructor. If not present, throws. 7. C#: D.CalledFromConstructor() 8. Java: B.<init>() finishes 9. Java: D.<init>() calls ManagedPeer.runConstructor(). 10. C#: ManagedPeer.RunConstructor() finds specified ConstructorInfo, invokes on instance created in (6). Note the Managed peer constructor invocations in (6) and (10), both on the same instance. What's with this Reflection for FormatterServices.GetUninitializedObject()? FormatterServices.GetUninitializedObject() isn't present in any PCL Profile (doh!), but it's the only mechanism I know of that allows me to not require a special activation constructor. Additionally, FormatterServices.GetUninitializedObject() IS supported in CoreCL, so this should work on all interesting runtimes. [0]: One exception being "framework peers", which don't do anything special in their constructors to cause managed peers to be created.
1 parent 8dd516b commit 1c99956

16 files changed

+364
-53
lines changed

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,33 @@ static Expression GetRuntime ()
326326
var runtime = Expression.Property (null, env, "Runtime");
327327
return runtime;
328328
}
329+
330+
static MethodInfo FormatterServices_GetUninitializedObject = Type.GetType ("System.Runtime.Serialization.FormatterServices", throwOnError: true)
331+
.GetRuntimeMethod ("GetUninitializedObject", new[]{typeof (Type)});
332+
static MethodInfo IJavaPeerable_SetPeerReference = typeof (IJavaPeerable).GetRuntimeMethod ("SetPeerReference", new[]{typeof (JniObjectReference)});
333+
334+
public override Expression<Func<ConstructorInfo, JniObjectReference, object[], object>> CreateConstructActivationPeerExpression (ConstructorInfo constructor)
335+
{
336+
if (constructor == null)
337+
throw new ArgumentNullException (nameof (constructor));
338+
339+
Func<object, object[], object> mbi = constructor.Invoke;
340+
341+
var c = Expression.Parameter (typeof (ConstructorInfo), "constructor");
342+
var r = Expression.Parameter (typeof (JniObjectReference), "reference");
343+
var p = Expression.Parameter (typeof (object[]), "parameters");
344+
345+
var t = Expression.Variable (typeof (Type), "type");
346+
var s = Expression.Variable (typeof (object), "self");
347+
var b = Expression.Block (
348+
new []{t, s},
349+
Expression.Assign (t, Expression.Property (c, "DeclaringType")),
350+
Expression.Assign (s, Expression.Call (FormatterServices_GetUninitializedObject, t)),
351+
Expression.Call (Expression.Convert (s, typeof (IJavaPeerable)), IJavaPeerable_SetPeerReference, r),
352+
Expression.Call (c, mbi.GetMethodInfo (), s, p),
353+
s);
354+
return Expression.Lambda<Func<ConstructorInfo, JniObjectReference, object[], object>> (b, new []{c, r, p});
355+
}
329356
}
330357

331358
static class JniSignature {

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

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Linq.Expressions;
45
using System.Reflection;
56
using System.Threading;
67

@@ -162,7 +163,7 @@ public void CreateMarshalFromJniMethodExpression_InstanceAction ()
162163
{
163164
var t = typeof (ExportTest);
164165
var m = t.GetMethod ("InstanceAction");
165-
CheckCreateInvocationExpression (null, t, m, typeof (Action<IntPtr, IntPtr>),
166+
CheckCreateMarshalToManagedExpression (null, t, m, typeof (Action<IntPtr, IntPtr>),
166167
@"void (IntPtr __jnienv, IntPtr __this)
167168
{
168169
JniTransition __envp;
@@ -188,28 +189,33 @@ public void CreateMarshalFromJniMethodExpression_InstanceAction ()
188189
}");
189190
}
190191

191-
static void CheckCreateInvocationExpression (JavaCallableAttribute export, Type type, MethodInfo method, Type expectedDelegateType, string expectedBody)
192+
static void CheckCreateMarshalToManagedExpression (JavaCallableAttribute export, Type type, MethodInfo method, Type expectedDelegateType, string expectedBody)
192193
{
193-
export = export ?? new JavaCallableAttribute ();
194-
var b = CreateBuilder ();
195-
var l = b.CreateMarshalToManagedExpression (method, export, type);
196-
Console.WriteLine ("## method: {0}", method.Name);
197-
Console.WriteLine (l.ToCSharpCode ());
194+
export = export ?? new JavaCallableAttribute ();
195+
var b = CreateBuilder ();
196+
var l = b.CreateMarshalToManagedExpression (method, export, type);
197+
CheckExpression (l, method.Name, expectedDelegateType, expectedBody);
198+
}
199+
200+
static void CheckExpression (LambdaExpression expression, string memberName, Type expressionType, string expectedBody)
201+
{
202+
Console.WriteLine ("## member: {0}", memberName);
203+
Console.WriteLine (expression.ToCSharpCode ());
198204
var da = AppDomain.CurrentDomain.DefineDynamicAssembly(
199205
new AssemblyName("dyn"), // call it whatever you want
200206
System.Reflection.Emit.AssemblyBuilderAccess.Save);
201207

202-
var _name = "dyn-" + method.Name + ".dll";
208+
var _name = "dyn-" + memberName + ".dll";
203209
var dm = da.DefineDynamicModule("dyn_mod", _name);
204210
var dt = dm.DefineType ("dyn_type", TypeAttributes.Public);
205211
var mb = dt.DefineMethod(
206-
method.Name,
212+
memberName,
207213
MethodAttributes.Public | MethodAttributes.Static);
208214

209-
l.CompileToMethod(mb);
215+
expression.CompileToMethod (mb);
210216
dt.CreateType();
211-
Assert.AreEqual (expectedDelegateType, l.Type);
212-
Assert.AreEqual (expectedBody, l.ToCSharpCode ());
217+
Assert.AreEqual (expressionType, expression.Type);
218+
Assert.AreEqual (expectedBody, expression.ToCSharpCode ());
213219
#if !__ANDROID__
214220
da.Save (_name);
215221
#endif // !__ANDROID__
@@ -220,7 +226,7 @@ public void CreateMarshalFromJniMethodExpression_StaticAction ()
220226
{
221227
var t = typeof (ExportTest);
222228
Action a = ExportTest.StaticAction;
223-
CheckCreateInvocationExpression (null, t, a.Method, typeof(Action<IntPtr, IntPtr>),
229+
CheckCreateMarshalToManagedExpression (null, t, a.Method, typeof(Action<IntPtr, IntPtr>),
224230
@"void (IntPtr __jnienv, IntPtr __class)
225231
{
226232
JniTransition __envp;
@@ -252,7 +258,7 @@ public void CreateMarshalFromJniMethodExpression_StaticActionInt32String ()
252258
var e = new JavaCallableAttribute () {
253259
Signature = "(ILjava/lang/String;)V",
254260
};
255-
CheckCreateInvocationExpression (e, t, m.Method, typeof (Action<IntPtr, IntPtr, int, IntPtr>),
261+
CheckCreateMarshalToManagedExpression (e, t, m.Method, typeof (Action<IntPtr, IntPtr, int, IntPtr>),
256262
@"void (IntPtr __jnienv, IntPtr __class, int i, IntPtr v)
257263
{
258264
JniTransition __envp;
@@ -286,7 +292,7 @@ public void CreateMarshalFromJniMethodExpression_StaticFuncMyLegacyColorMyColor_
286292
var e = new JavaCallableAttribute () {
287293
Signature = "(II)I",
288294
};
289-
CheckCreateInvocationExpression (e, t, m.Method, typeof (Func<IntPtr, IntPtr, int, int, int>),
295+
CheckCreateMarshalToManagedExpression (e, t, m.Method, typeof (Func<IntPtr, IntPtr, int, int, int>),
290296
@"int (IntPtr __jnienv, IntPtr __class, int color1, int color2)
291297
{
292298
JniTransition __envp;
@@ -327,7 +333,7 @@ public void CreateMarshalFromJniMethodExpression_FuncInt64 ()
327333
var e = new JavaCallableAttribute () {
328334
Signature = "()J",
329335
};
330-
CheckCreateInvocationExpression (e, t, m, typeof (Func<IntPtr, IntPtr, long>),
336+
CheckCreateMarshalToManagedExpression (e, t, m, typeof (Func<IntPtr, IntPtr, long>),
331337
@"long (IntPtr __jnienv, IntPtr __this)
332338
{
333339
JniTransition __envp;
@@ -364,7 +370,7 @@ public void CreateMarshalFromJniMethodExpression_FuncIJavaObject ()
364370
var e = new JavaCallableAttribute () {
365371
Signature = "()Ljava/lang/Object;",
366372
};
367-
CheckCreateInvocationExpression (e, t, m, typeof (Func<IntPtr, IntPtr, IntPtr>),
373+
CheckCreateMarshalToManagedExpression (e, t, m, typeof (Func<IntPtr, IntPtr, IntPtr>),
368374
@"IntPtr (IntPtr __jnienv, IntPtr __this)
369375
{
370376
JniTransition __envp;
@@ -418,7 +424,7 @@ public void CreateMarshalToManagedExpression_DirectMethod ()
418424
var e = new JavaCallableAttribute () {
419425
Signature = "()V",
420426
};
421-
CheckCreateInvocationExpression (e, a.Method.DeclaringType, a.Method, typeof (Action<IntPtr, IntPtr>),
427+
CheckCreateMarshalToManagedExpression (e, a.Method.DeclaringType, a.Method, typeof (Action<IntPtr, IntPtr>),
422428
@"void (IntPtr jnienv, IntPtr context)
423429
{
424430
JniTransition __envp;
@@ -441,5 +447,34 @@ public void CreateMarshalToManagedExpression_DirectMethod ()
441447
}
442448
}");
443449
}
444-
}
450+
451+
[Test]
452+
public void CreateConstructActivationPeerExpression_Exceptions ()
453+
{
454+
var b = CreateBuilder ();
455+
Assert.Throws<ArgumentNullException> (() => b.CreateConstructActivationPeerExpression (null));
456+
}
457+
458+
[Test]
459+
public void CreateConstructActivationPeerExpression ()
460+
{
461+
var b = CreateBuilder ();
462+
var c = typeof (ExportedMemberBuilderTest).GetConstructor (new Type [0]);
463+
var e = b.CreateConstructActivationPeerExpression (c);
464+
CheckExpression (e,
465+
"ExportedMemberBuilderTest_ctor",
466+
typeof(Func<ConstructorInfo, JniObjectReference, object[], object>),
467+
@"object (ConstructorInfo constructor, JniObjectReference reference, object[] parameters)
468+
{
469+
Type type;
470+
object self;
471+
472+
type = constructor.DeclaringType;
473+
self = FormatterServices.GetUninitializedObject(type);
474+
(IJavaPeerable)self.SetPeerReference(reference);
475+
constructor.Invoke(self, parameters);
476+
return self;
477+
}");
478+
}
479+
}
445480
}

src/Java.Interop/Java.Interop/JavaException.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ unsafe public class JavaException : Exception, IJavaPeerable
2929
protected static readonly JniObjectReference* InvalidJniObjectReference = null;
3030

3131
public unsafe JavaException ()
32+
: this (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
3233
{
34+
if (PeerReference.IsValid)
35+
return;
36+
3337
var peer = JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType (), null);
3438
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
3539
JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this, null);
@@ -73,14 +77,9 @@ public unsafe JavaException (string message, Exception innerException)
7377
public JavaException (ref JniObjectReference reference, JniObjectReferenceOptions transfer)
7478
: base (_GetMessage (ref reference, transfer), _GetCause (ref reference, transfer))
7579
{
76-
if (transfer == JniObjectReferenceOptions.None)
77-
return;
78-
79-
if (!reference.IsValid)
80-
return;
81-
8280
Construct (ref reference, transfer);
83-
javaStackTrace = _GetJavaStack (PeerReference);
81+
if (PeerReference.IsValid)
82+
javaStackTrace = _GetJavaStack (PeerReference);
8483
}
8584

8685
protected void Construct (ref JniObjectReference reference, JniObjectReferenceOptions options)

src/Java.Interop/Java.Interop/JavaObject.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ public virtual JniPeerMembers JniPeerMembers {
5959

6060
public JavaObject (ref JniObjectReference reference, JniObjectReferenceOptions options)
6161
{
62-
if (options == JniObjectReferenceOptions.None)
63-
return;
64-
6562
Construct (ref reference, options);
6663
}
6764

6865
public unsafe JavaObject ()
66+
: this (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
6967
{
68+
if (PeerReference.IsValid)
69+
return;
70+
7071
var peer = JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType (), null);
7172
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
7273
JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this, null);

src/Java.Interop/Java.Interop/JniObjectReference.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,9 @@ public static void Dispose (ref JniObjectReference reference)
197197

198198
public static void Dispose (ref JniObjectReference reference, JniObjectReferenceOptions options)
199199
{
200+
if (options == JniObjectReferenceOptions.None)
201+
return;
202+
200203
if (!reference.IsValid)
201204
return;
202205

src/Java.Interop/Java.Interop/JniRuntime.JniExportedMemberBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ public Delegate CreateMarshalToM
6262

6363
public abstract LambdaExpression CreateMarshalToManagedExpression (MethodInfo method);
6464
public abstract IEnumerable<JniNativeMethodRegistration> GetExportedMemberRegistrations (Type declaringType);
65+
66+
public abstract Expression<Func<ConstructorInfo, JniObjectReference, object[], object>> CreateConstructActivationPeerExpression (ConstructorInfo constructor);
67+
68+
public Func<ConstructorInfo, JniObjectReference, object[], object> CreateConstructActivationPeerFunc (ConstructorInfo constructor)
69+
{
70+
if (constructor == null)
71+
throw new ArgumentNullException (nameof (constructor));
72+
73+
var e = CreateConstructActivationPeerExpression (constructor);
74+
return e.Compile ();
75+
}
6576
}
6677
}
6778
}

src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -72,43 +72,47 @@ protected virtual void Dispose (bool disposing)
7272

7373
public abstract List<JniSurfacedPeerInfo> GetSurfacedPeers ();
7474

75-
public void Construct (IJavaPeerable value, ref JniObjectReference reference, JniObjectReferenceOptions options)
75+
public void Construct (IJavaPeerable peer, ref JniObjectReference reference, JniObjectReferenceOptions options)
7676
{
77-
if (value == null)
78-
throw new ArgumentNullException (nameof (value));
77+
if (peer == null)
78+
throw new ArgumentNullException (nameof (peer));
7979

80-
var activationRef = value.PeerReference;
81-
if (activationRef.IsValid) {
82-
value.SetJniManagedPeerState (value.JniManagedPeerState | JniManagedPeerStates.Activatable);
80+
var newRef = peer.PeerReference;
81+
if (newRef.IsValid) {
82+
// Activation! See ManagedPeer.RunConstructor
83+
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Activatable);
8384
JniObjectReference.Dispose (ref reference, options);
84-
reference = activationRef;
85-
options = JniObjectReferenceOptions.Copy;
86-
}
85+
newRef = newRef.NewGlobalRef ();
86+
} else if (options == JniObjectReferenceOptions.None) {
87+
// `reference` is likely *InvalidJniObjectReference, and can't be touched
88+
return;
89+
} else if (!reference.IsValid) {
90+
throw new ArgumentException ("JNI Object Reference is invalid.", nameof (reference));
91+
} else {
92+
newRef = reference;
8793

88-
if (!reference.IsValid)
89-
throw new ArgumentException ("handle is invalid.", nameof (reference));
94+
if ((options & JniObjectReferenceOptions.Copy) == JniObjectReferenceOptions.Copy) {
95+
newRef = reference.NewGlobalRef ();
96+
}
9097

91-
var newRef = reference;
92-
if ((options & JniObjectReferenceOptions.Copy) == JniObjectReferenceOptions.Copy) {
93-
newRef = reference.NewGlobalRef ();
98+
JniObjectReference.Dispose (ref reference, options);
9499
}
95-
value.SetPeerReference (newRef);
96-
JniObjectReference.Dispose (ref reference, options);
97100

98-
value.SetJniIdentityHashCode (JniSystem.IdentityHashCode (newRef));
101+
peer.SetPeerReference (newRef);
102+
peer.SetJniIdentityHashCode (JniSystem.IdentityHashCode (newRef));
99103

100104
var o = Runtime.ObjectReferenceManager;
101105
if (o.LogGlobalReferenceMessages) {
102106
o.WriteGlobalReferenceLine ("Created PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}, Java.Type={4}",
103107
newRef.ToString (),
104-
value.JniIdentityHashCode.ToString ("x"),
105-
RuntimeHelpers.GetHashCode (value).ToString ("x"),
106-
value.GetType ().FullName,
108+
peer.JniIdentityHashCode.ToString ("x"),
109+
RuntimeHelpers.GetHashCode (peer).ToString ("x"),
110+
peer.GetType ().FullName,
107111
JniEnvironment.Types.GetJniTypeNameFromInstance (newRef));
108112
}
109113

110114
if ((options & DoNotRegisterTarget) != DoNotRegisterTarget) {
111-
Add (value);
115+
Add (peer);
112116
}
113117
}
114118

0 commit comments

Comments
 (0)