Skip to content

Commit f5ce0ad

Browse files
committed
[java-interop, Java.Interop] Start plumbing a proper Mono GC Bridge
Context: #13 What do we want? For cross-VM object references to work -- a java.util.ArrayList referencing a MyJavaObjectSubclass instance, and being the *only* reference, yet the MyJavaObjectSubclass instance isn't collected, *because* the Java-side ArrayList is keeping it alive. See also commit eb69a47. What we currently have is a terrible solution. (Even as a terrible solution, it's a terrible solution!) Fortunately, we know what we need to do: import the Xamarin.Android GC bridge implementation, which is (reasonably) well debugged and has been used for *years*. Update the java-interop native library, adding a new JavaInteropGCBridge API (in <java-interop-gc-bridge.h>), and implement that API for use on Mono. (Currently unknown: how feasible it'll be to provide a similar API for use with CoreCLR, which is an entirely different can of worms...) Changes from the Xamarin.Android GC bridge are as follows: * It's (mostly!) scoped to an instance of JavaInteropGCBridge, as opposed to the Xamarin.Android implementation, which is dozens of global variables. (There's still a global, but now it's just `mono_bridge`, which is a pointer to an easier-to-reason-about struct. * Code has been reformatted for consistency, and to conform to https://matt.sh/howto-c , which in turn follows common C++ practice -- declare variables at initialization, etc. * The JNIEnv.BridgeProcessing field is now the MonoRuntimeValueManager.GCBridgeProcessingIsActive field, though the *actual* field to use can be specified via java_interop_gc_bridge_set_bridge_processing_field(). * Instead of always using Java.Lang.Object and Java.Land.Throwable (from Mono.Android.dll) as brided types, allow the bridged types to be specified via java_interop_gc_bridge_register_bridgeable_type(). ...which is to say that there are some stylistic changes, but no concrete algorithm changes or structural changes to the GC bridge itself (aside from some function re-ordering). With the JavaInteropGCBridge API in place, *integrate* it with Java.Interop proper, which in turn is kinda complicated, as the GC bridge is specific to Mono (+SGEN!), and will not run on .NET. To square this circle, we make JniRuntime.JniValueManager an *abstract* type, with a new abstract WaitForGCBridgeProcessing() method, and concretely implement this type in Java.Interop.MonoRuntimeValueManager (in Java.Runtime.Environment.dll). We then do a runtime check to see if we're on mono; if we are, we use MonoRuntimeValueManager; otherwise, we through an exception so whoever is setting things up can provide their own JniRuntime.JniValueManager implementation. Update ExportedMemberBuilder so that JniEnvironment.Runtime.ValueManager.WaitForGCBridgeProcessing() is called at entry from unmanaged to managed code, as is done with JNIEnv.WaitForGCBridgeProcessing() in Xamarin.Android. Notes: * Xamarin.Android used mono_assembly_load_full() in order to load Mono.Android.dll and find the appropriate types. Trying to do this in java-interop caused a crash for an unexplored reason. To avoid the crash, mono_assembly_load_full() is *skipped* entirely. Instead, a System.RuntimeTypeHandle is passed from managed code to java-interop to act upon the intended types. This in turn raises a concern, as we're passing the System.RuntimeTypeHandle *structure* to P/Invoke, which *works*, but I don't know how *stable* that is gauranteed to be. *Better* would be to use RuntimeTypeHandle.Value (an IntPtr), but for some asinine reason that property is NOT in PCL Profile7, meaning I can't access it from a PCL lib. Question: How problematic is this? * The JavaInteropGCBridge logging implementation records the thread name and thread ID of the thread creating/destroying the JNI handle. This is currently done by registering a delegate from managed code, which uses Thread.CurrentThread.Name and Thread.ManagedThreadId. WANTED: A Mono embedding API which provides a way to get the thread name and thread id of a MonoThread* value. This would avoid an unmanaged > managed transition during logging. * The GC bridge sets the static MonoRuntimeValueManager.GCBridgeProcessingIsActive field within *all* AppDomains. This is so that Java.Interop can be used in multiple AppDomains...because why not? However, this is problematic because we're using mono_class_vtable() to get the MonoVTable* to set the static field, and mono_class_vtable() *always loads the type*, which is inappropriate if Java.Runtime.Environment.dll hasn't been loaded into the current AppDomain. WANTED: A Mono embedding API like mono_class_vtable() which returns NULL if the type hasn't already been loaded into the specified MonoDomain*. That way we can set MonoRuntimeValueManager.GCBridgeProcessingIsActive within the AppDomains that have it, but not *every* AppDomain. TODO: While this commit *adds* a GC bridge implementation, it is not currently *used*. Here are some of the things that need to be done to do that, among others: * Review the JniRuntime.JniValueManager API for sanity. Ideally it should be possible to implement "sanely" on .NET, though possibly *without* GC integration. Perhaps "manual" GCs are supportable, e.g. a JniRuntime.ValueManager.Collect() method which the developer must invoke occasionally. (Terrible, certainly, but possibly useful for prototypes?) * SafeHandles must be dropped -- disable FEATURE_JNIENVIRONMENT_SAFEHANDLES and use FEATURE_JNIENVIRONMENT_JI_PINVOKES (commit add10d1). JavaInteropGCBridge only knows about "handle" and "handle_type" fields (among others); it doesn't support SafeHandles, so in order for Java.Interop.JavaObject to be usable with JavaInteropGCBridge, we need to drop SafeHandle use. * JavaInteropGCBridge requires that the Java Callable Wrapper (formerly Android Callable Wrapper) provide methods named monodroidAddReference(java.lang.Object) and monodroidClearReferences(). No Java types within Java.Interop currently provide these methods, meaning the *very crucial glue* to support cross-VM object references is *missing*. We should provide a variant on the Xamarin.Android mono.android.IGCUserPeer interface for use with Java.Interop, and improve JavaInteropGCBridge to use the interface instead of looking up the jmethodID values afresh for every encountered type. * We have *two* separate JNI handle loggers now: one in java-interop, and one in LoggingJniObjectReferenceManagerDecorator. We should kill the LoggingJniObjectReferenceManagerDecorator type.
1 parent 8c4248b commit f5ce0ad

File tree

16 files changed

+1413
-20
lines changed

16 files changed

+1413
-20
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ public LambdaExpression CreateMarshalToManagedExpression (MethodInfo method, Jav
136136
Expression.Assign (envp, CreateJniTransition (jnienv)),
137137
};
138138

139+
var waitForGCBridge = typeof(JniRuntime.JniValueManager)
140+
.GetRuntimeMethod (nameof (JniRuntime.JniValueManager.WaitForGCBridgeProcessing), new Type [0]);
139141
var marshalBody = new List<Expression> () {
140142
Expression.Assign (jvm, GetRuntime ()),
143+
Expression.Call (Expression.Property (jvm, "ValueManager"), waitForGCBridge),
141144
};
142145

143146
Expression self = null;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ public void CreateMarshalFromJniMethodExpression_InstanceAction ()
173173
try
174174
{
175175
__jvm = JniEnvironment.Runtime;
176+
__jvm.ValueManager.WaitForGCBridgeProcessing();
176177
__this_val = __jvm.ValueManager.GetValue<ExportTest>(__this);
177178
__this_val.InstanceAction();
178179
}
@@ -229,6 +230,7 @@ public void CreateMarshalFromJniMethodExpression_StaticAction ()
229230
try
230231
{
231232
__jvm = JniEnvironment.Runtime;
233+
__jvm.ValueManager.WaitForGCBridgeProcessing();
232234
ExportTest.StaticAction();
233235
}
234236
catch (Exception __e)
@@ -261,6 +263,7 @@ public void CreateMarshalFromJniMethodExpression_StaticActionInt32String ()
261263
try
262264
{
263265
__jvm = JniEnvironment.Runtime;
266+
__jvm.ValueManager.WaitForGCBridgeProcessing();
264267
v_val = Strings.ToString(v);
265268
ExportTest.StaticActionInt32String(i, v_val);
266269
}
@@ -297,6 +300,7 @@ public void CreateMarshalFromJniMethodExpression_StaticFuncMyLegacyColorMyColor_
297300
try
298301
{
299302
__jvm = JniEnvironment.Runtime;
303+
__jvm.ValueManager.WaitForGCBridgeProcessing();
300304
color1_val = new MyLegacyColor(color1);
301305
color2_val = new MyColor(color2);
302306
__mret = ExportTest.StaticFuncMyLegacyColorMyColor_MyColor(color1_val, color2_val);
@@ -335,6 +339,7 @@ public void CreateMarshalFromJniMethodExpression_FuncInt64 ()
335339
try
336340
{
337341
__jvm = JniEnvironment.Runtime;
342+
__jvm.ValueManager.WaitForGCBridgeProcessing();
338343
__this_val = __jvm.ValueManager.GetValue<ExportTest>(__this);
339344
__mret = __this_val.FuncInt64();
340345
return __mret;
@@ -374,6 +379,7 @@ public void CreateMarshalFromJniMethodExpression_FuncIJavaObject ()
374379
try
375380
{
376381
__jvm = JniEnvironment.Runtime;
382+
__jvm.ValueManager.WaitForGCBridgeProcessing();
377383
__this_val = __jvm.ValueManager.GetValue<ExportTest>(__this);
378384
__mret = __this_val.FuncIJavaObject();
379385
if (null == __mret)
@@ -422,6 +428,7 @@ public void CreateMarshalToManagedExpression_DirectMethod ()
422428
try
423429
{
424430
__jvm = JniEnvironment.Runtime;
431+
__jvm.ValueManager.WaitForGCBridgeProcessing();
425432
ExportedMemberBuilderTest.DirectInvocation(jnienv, context);
426433
}
427434
catch (Exception __e)

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Linq.Expressions;
55
using System.Reflection;
66
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
79

810
using Java.Interop.Expressions;
911

@@ -19,10 +21,15 @@ partial class CreationOptions {
1921

2022
partial void SetValueManager (CreationOptions options)
2123
{
22-
ValueManager = SetRuntime (options.ValueManager ?? new JniValueManager ());
24+
var manager = options.ValueManager;
25+
if (manager == null)
26+
throw new ArgumentException (
27+
"No JniValueManager specified in JniRuntime.CreationOptions.ValueManager.",
28+
nameof (options));
29+
ValueManager = SetRuntime (manager);
2330
}
2431

25-
public partial class JniValueManager : ISetRuntime, IDisposable {
32+
public abstract partial class JniValueManager : ISetRuntime, IDisposable {
2633

2734
public JniRuntime Runtime { get; private set; }
2835

@@ -47,11 +54,15 @@ protected virtual void Dispose (bool disposing)
4754

4855
foreach (var o in RegisteredInstances.Values) {
4956
var t = (IDisposable) o.Target;
57+
if (t == null)
58+
continue;
5059
t.Dispose ();
5160
}
5261
RegisteredInstances.Clear ();
5362
}
5463

64+
public abstract void WaitForGCBridgeProcessing ();
65+
5566
Dictionary<int, WeakReference> RegisteredInstances = new Dictionary<int, WeakReference>();
5667

5768
public List<WeakReference> GetSurfacedObjects ()

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,17 @@ protected JniRuntime (CreationOptions options)
171171
TrackIDs = options.TrackIDs;
172172
DestroyRuntimeOnDispose = options.DestroyRuntimeOnDispose;
173173

174-
ObjectReferenceManager = SetRuntime (options.ObjectReferenceManager ?? new JniObjectReferenceManager ());
175-
TypeManager = SetRuntime (options.TypeManager ?? new JniTypeManager ());
176-
177-
SetValueManager (options);
178-
SetExportedMemberBuilder (options);
179-
180174
NewObjectRequired = options.NewObjectRequired;
181175

182176
InvocationPointer = options.InvocationPointer;
183177
Invoker = CreateInvoker (InvocationPointer);
184178

179+
SetValueManager (options);
180+
SetExportedMemberBuilder (options);
181+
182+
ObjectReferenceManager = SetRuntime (options.ObjectReferenceManager ?? new JniObjectReferenceManager ());
183+
TypeManager = SetRuntime (options.TypeManager ?? new JniTypeManager ());
184+
185185
if (Interlocked.CompareExchange (ref current, this, null) != null) {
186186
Debug.WriteLine ("WARNING: More than one JniRuntime instance created. This is DOOMED TO FAIL.");
187187
}

src/Java.Interop/Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class JniRuntimeJniValueManagerTests : JavaVMFixture {
1212
[Test]
1313
public void CreateValue ()
1414
{
15-
using (var vm = new JniRuntime.JniValueManager ())
15+
using (var vm = new MyValueManager ())
1616
using (var o = new JavaObject ()) {
1717
vm.OnSetRuntime (JniRuntime.CurrentRuntime);
1818

@@ -27,6 +27,13 @@ public void CreateValue ()
2727
}
2828
}
2929

30+
class MyValueManager : JniRuntime.JniValueManager {
31+
32+
public override void WaitForGCBridgeProcessing ()
33+
{
34+
}
35+
}
36+
3037
[Test]
3138
public void GetValue_ReturnsAlias ()
3239
{

src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public JreRuntimeOptions ()
3838
Path.GetDirectoryName (typeof (JreRuntimeOptions).Assembly.Location),
3939
"java-interop.jar"),
4040
};
41+
42+
if (ValueManager == null && Type.GetType ("Mono.Runtime", throwOnError: false) != null)
43+
ValueManager = new MonoRuntimeValueManager ();
4144
}
4245

4346
public JreRuntimeOptions AddOption (string option)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace Java.Interop {
5+
6+
enum GCBridgeUseWeakReferenceKind {
7+
Java,
8+
Jni,
9+
}
10+
11+
class MonoRuntimeValueManager : JniRuntime.JniValueManager {
12+
13+
#pragma warning disable 0649
14+
// This field is mutated by the java-interop native lib
15+
static volatile bool GCBridgeProcessingIsActive;
16+
#pragma warning restore 0649
17+
18+
GetThreadDescriptionCb getThreadDescription;
19+
20+
public override void OnSetRuntime (JniRuntime runtime)
21+
{
22+
base.OnSetRuntime (runtime);
23+
getThreadDescription = x => java_interop_strdup (runtime.GetCurrentThreadDescription ());
24+
25+
var bridge = java_interop_gc_bridge_get_current ();
26+
if (bridge != IntPtr.Zero)
27+
return;
28+
29+
bridge = java_interop_gc_bridge_new (runtime.InvocationPointer);
30+
if (bridge == IntPtr.Zero)
31+
throw new NotSupportedException ("Could not initialize JNI::Mono GC Bridge!");
32+
33+
try {
34+
if (java_interop_gc_bridge_set_thread_description_creator (bridge, getThreadDescription, IntPtr.Zero) < 0)
35+
throw new NotSupportedException ("Could not set thread description creator!");
36+
if (java_interop_gc_bridge_set_bridge_processing_field (bridge, typeof (MonoRuntimeValueManager).TypeHandle, nameof (GCBridgeProcessingIsActive)) < 0)
37+
throw new NotSupportedException ("Could not set bridge processing field!");
38+
foreach (var t in new[]{typeof (JavaObject), typeof (JavaException)}) {
39+
if (java_interop_gc_bridge_register_bridgeable_type (bridge, t.TypeHandle) < 0)
40+
throw new NotSupportedException ("Could not register type " + t.FullName + "!");
41+
}
42+
if (java_interop_gc_bridge_set_current_once (bridge) < 0)
43+
throw new NotSupportedException ("Could not set GC Bridge instance!");
44+
}
45+
catch (Exception) {
46+
java_interop_gc_bridge_free (bridge);
47+
throw;
48+
}
49+
if (java_interop_gc_bridge_register_hooks_once (GCBridgeUseWeakReferenceKind.Jni) < 0)
50+
throw new NotSupportedException ("Could not register GC Bridge with Mono!");
51+
}
52+
53+
public override void WaitForGCBridgeProcessing ()
54+
{
55+
if (!GCBridgeProcessingIsActive)
56+
return;
57+
java_interop_gc_bridge_wait_for_bridge_processing ();
58+
}
59+
60+
const string JavaInteropLib = "java-interop";
61+
62+
delegate IntPtr GetThreadDescriptionCb (IntPtr user_data);
63+
64+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
65+
static extern IntPtr java_interop_gc_bridge_get_current ();
66+
67+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
68+
static extern int java_interop_gc_bridge_set_current_once (IntPtr bridge);
69+
70+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
71+
static extern int java_interop_gc_bridge_register_hooks_once (GCBridgeUseWeakReferenceKind weak_ref_kind);
72+
73+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
74+
static extern IntPtr java_interop_gc_bridge_new (IntPtr jvm);
75+
76+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
77+
static extern int java_interop_gc_bridge_free (IntPtr bridge);
78+
79+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
80+
static extern int java_interop_gc_bridge_set_thread_description_creator (IntPtr bridge, GetThreadDescriptionCb creator, IntPtr user_data);
81+
82+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
83+
static extern IntPtr java_interop_strdup (string value);
84+
85+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
86+
static extern int java_interop_gc_bridge_set_bridge_processing_field (IntPtr bridge, RuntimeTypeHandle type_handle, string field_name);
87+
88+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
89+
static extern int java_interop_gc_bridge_register_bridgeable_type (IntPtr bridge, RuntimeTypeHandle type_handle);
90+
91+
[DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)]
92+
static extern void java_interop_gc_bridge_wait_for_bridge_processing ();
93+
}
94+
}
95+

src/Java.Runtime.Environment/Java.Runtime.Environment.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<ItemGroup>
3535
<Compile Include="Properties\AssemblyInfo.cs" />
3636
<Compile Include="Java.Interop\JreRuntime.cs" />
37+
<Compile Include="Java.Interop\MonoRuntimeValueManager.cs" />
3738
</ItemGroup>
3839
<ItemGroup>
3940
<Reference Include="System" />

0 commit comments

Comments
 (0)