Skip to content

Commit 972c5bc

Browse files
committed
[Java.Interop] Avoid JNIEnv::NewObject().
(This unexpectedly gigantic 2653 line patch brought to you by Isanity, Experience, and YOU DON'T KNOW, MAN! YOU WEREN'T THERE!) JNI, in its infinite wisdom, has two different mechanisms to create an instance of a Java object: the easy way, and the correct way. 1. The Easy Way: JNIEnv::NewObject() 2. The Correct Way: JNIEnv::AllocObject() + JNIEnv::CallNonvirtualVoidMethod() The easy way is JNIEnv::NewObject(class, constructor, args): give it the type, the constructor, and (optional) arguments, and you get back an object. The correct way is JNIEnv::AllocObject(), which _creates_ the object but DOES NOT RUN THE CONSTRUCTOR, followed by JNIEnv::CallNonvirtualVoidMethod(), which executes the constructor. Why does it matter? Leaky abstractions, and virtual method invocation from the constructor. Just as with C#, and unlike C++, in Java virtual methods always resolve to the most derived implementation: // C# class B {public B() {Console.WriteLine("B..ctor"); CalledFromConstructor();} public virtual void CalledFromConstructor() {}} class D : B {public D() {Console.WriteLine ("d..ctor");} public override void CalledFromConstructor() {Console.WriteLine("D.CalledFromConstructor");}} // ... new D(); The above prints: B..ctor D.CalledFromConstructor d..ctor Note that D.M()is invoked _before_ the D constructor executes! (In actuality _part_ of the D constructor executes _first_, but that's the part that ~immediately calls the B constructor. The only way to intervene in this chain is with member initializers, and "hacks" with constructor parameters.) The "problem" is that the same thing happens in Java, and if `D` has a native method implemented in C#, and JNIEnv::NewObject() is used, then things go all "funky," because the JNI method marshaler could be called before there's a C# instance to invoke the method upon: // Hypothetical D.CalledFromConstructorHandler impl partial class D { static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr self) { var self = JavaVM.Current.GetObject<D>(n_self); self.CalledFromConstructor (); } } So our runtime behavior would be: C#: D..ctor(), eventually calls JNIEnv::NewObject() Java: B.<init>() Java: B.calledFromConstructor() Java: D.n_calledFromConstructor(); C#: D.CalledFromConstructorHandler() C#: JavaVM.GetObject(): no object registered At this point one of two things will happen: Option 1: D has a (JniReferenceSafeHandle, JniHandleOwnership) constructor: C#: JavaVM.GetObject() creates a new "dummy" D C#: D.CalledFromConstructorHandler() calls D.CalledFromConstructor(). The problem is that at the end of this, the "original" execution path is referencing _one_ 'D' instance, which is unrelated to the 'D' instance that D.CalledFromConstructorHandler() created. There are _two_ 'D' instances! Insanity quickly follows. Option 2: D doesn't have a (JniReferenceSafeHandle, JniHandleOwnership) constructor. C#: JavaVM.GetObject() throws an exception **BOOM** Becuase we don't have correct exception propogation implemented yet, this results in corrupting OpenJDK (we unwound native Java stack frames!), and the process later aborts: malloc: *** error for object 0x79d8c248: Non-aligned pointer being freed *** set a breakpoint in malloc_error_break to debug Stacktrace: at <unknown> <0xffffffff> at (wrapper managed-to-native) object.wrapper_native_0x3a47b05 (Java.Interop.JniEnvironmentSafeHandle,Java.Interop.JniReferenceSafeHandle) <IL 0x00092, 0xffffffff> at Java.Interop.JniEnvironment/Handles.NewLocalRef (Java.Interop.JniReferenceSafeHandle) [0x0001b] in /Users/Shared/Dropbox/Developer/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.g.cs:881 at Java.Interop.JniReferenceSafeHandle.NewLocalRef () [0x00002] in /Users/Shared/Dropbox/Developer/Java.Interop/src/Java.Interop/Java.Interop/JniReferenceSafeHandle.cs:40 at Java.Interop.JavaVM.SetObjectSafeHandle<T> (T,Java.Interop.JniReferenceSafeHandle,Java.Interop.JniHandleOwnership) [0x00030] in /Users/Shared/Dropbox The above stacktrace is meaningless; the corruption happened long ago, and things are blowing up. So the problem with JNIEnv::NewObject() is that it implicitly requires creating "throwaway" instances, and very often you DO NOT WANT "throwaway" instances to be created! What's the fix then? JNIEnv::AllocObject(), which does NOT invoke the Java constructor. This allows us to (temporarily!) register the instance _during constructor execution_ so that JNI method marshalers can lookup the already created instance. This results in the saner control flow: C#: D..ctor(), eventually calls JNIEnv::AllocObject() C#: D instance is registered with AllocObject()'d handle. C#: JNIEnv::CallNonvirtualVoidMethod() invoked on B.<init>() Java: B.<init>() Java: B.calledFromConstructor() Java: D.n_calledFromConstructor(); C#: D.CalledFromConstructorHandler() C#: JavaVM.GetObject(): registered instance found, used. - execution returns back through B.<init>() to D..ctor() No hair is lost in the process. There is one "minor" problem here, though: Android. Specifically, Android prior to v3.0 Honeycomb doesn't properly support JNIEnv::AllocObject() + JNIEnv::CallNonvirtualVoidMethod(); it throws CloneNotSupportedException: https://code.google.com/p/android/issues/detail?id=13832 Using JNIEnv::NewObject() leads to insanity. Requiring JNIEnv::AllocObject() means things CANNOT work on Android < v3.0. The fix? Options! (Or don't support older Android versions...) Add a new JavaVMOptions.NewObjectRequired property: if True, then JNIEnv::NewObject() is used. If False (the default!), then JNIEnv::AllocObject() is used, sanity is retained, and everyone rejoices. (Yay.) (~150 lines to describe WHAT's being addressed!) How's this implemented? Through three sets of methods: 1. JniPeerInstanceMethods.StartCreateInstance() 2. JniPeerInstanceMethods.FinishCreateInstance() 3. JavaObject.SetSafeHandle(), JavaException.SetSafeHandle(). JniPeerInstanceMethods.StartCreateInstance() kicks things off: if JavaVMOptions.NewObjectRequired is True, then it's JNIEnv::NewObject(); if False, it's JNIEnv::AllocObject(). StartCreateInstance() returns the JNI handle. JniPeerInstanceMethods.FinishCreateInstance() does nothing if JavaVMOptions.NewObjectRequired is True; if false, then it calls JNIEnv::CallNonvirtualVoidMethod(). JavaObject.SetSafeHandle() and JavaException.SetSafeHandle() ties them all together: it takes the handle returned from JniPeerInstanceMethods.StartCreateInstance(), (optionally) registers the instance, and then (later) will unregister the instance. class SomeClass : JavaObject { public SomeClass(Args...) { using (SetSafeHandle ( JniPeerMembers.InstanceMethods.StartCreateInstance (JNI_SIGNATURE, GetType (), args...), JniHandleOwnership.Transfer)) { JniPeerMembers.InstanceMethods.FinishCreateInstance (JNI_SIGNATURE, this, args...); } } Flow of control (ideal case): 1. StartCreateInstance() calls JNIEnv::AllocObject() 2. SetSafeHandle() registers the handle::instance mapping, returns "Cleanup" instance. 3. FinishCreateInstance() calls JNIEnv::CallNonvirtualVoidMethod() to invoke the constructor. 4. "Cleanup".Dispose() unregisters the instance mapping from (2). Just as with JniPeerInstanceMethods.Call*Method(), JniPeerInstanceMethods.StartCreateInstance() and JniPeerInstanceMethods.FinishCreateInstance() are overloaded to generically marshal up to 16 parameters. Finally, CACHING: we need the appropriate jclass and jmethodID for the most derived subclass that we're creating. In Xamarin.Android these WERE NOT CACHED: they were looked up (and destroyed) when creating each instance of a type with an "Android Callable Wrapper" (generated Java "stub" type). Only jclass and constructor jmethodID instances for "normal" Java types were cached. JniPeerInstanceMethods DOES cache: it maintains a JniPeerInstanceMethods.SubclassConstructors mapping that tracks all encountered subclasses and their constructors, so that future instance creation doesn't need to relookup the jclass and jmethodID values.
1 parent f3c6c17 commit 972c5bc

16 files changed

+2220
-59
lines changed

src/Java.Interop/Java.Interop.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<ItemGroup>
3737
<Compile Include="Properties\AssemblyInfo.cs" />
3838
<Compile Include="Java.Interop\JavaVM.cs" />
39+
<Compile Include="Java.Interop\JniAllocObjectRef.cs" />
3940
<Compile Include="Java.Interop\JniReferenceSafeHandle.cs" />
4041
<Compile Include="Java.Interop\JniInstanceMethodID.cs" />
4142
<Compile Include="Java.Interop\JniEnvironment.cs" />

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

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,45 @@ public class JavaException : Exception, IJavaObject, IJavaObjectEx
1414

1515
public JavaException ()
1616
{
17-
JavaVM.SetObjectSafeHandle (this, _NewObject (GetType (), JniPeerMembers), JniHandleOwnership.Transfer);
17+
using (SetSafeHandle (
18+
JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType ()),
19+
JniHandleOwnership.Transfer)) {
20+
JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this);
21+
}
1822
javaStackTrace = _GetJavaStack (SafeHandle);
1923
}
2024

2125
public JavaException (string message)
2226
: base (message)
2327
{
24-
JavaVM.SetObjectSafeHandle (this, _NewObject (GetType (), JniPeerMembers), JniHandleOwnership.Transfer);
28+
const string signature = "(Ljava/lang/String;)V";
29+
using (SetSafeHandle (
30+
JniPeerMembers.InstanceMethods.StartCreateInstance (signature, GetType (), message),
31+
JniHandleOwnership.Transfer)) {
32+
JniPeerMembers.InstanceMethods.FinishCreateInstance (signature, this, message);
33+
}
2534
javaStackTrace = _GetJavaStack (SafeHandle);
2635
}
2736

2837
public JavaException (string message, Exception innerException)
2938
: base (message, innerException)
3039
{
31-
JavaVM.SetObjectSafeHandle (this, _NewObject (GetType (), JniPeerMembers), JniHandleOwnership.Transfer);
40+
const string signature = "(Ljava/lang/String;)V";
41+
using (SetSafeHandle (
42+
JniPeerMembers.InstanceMethods.StartCreateInstance (signature, GetType (), message),
43+
JniHandleOwnership.Transfer)) {
44+
JniPeerMembers.InstanceMethods.FinishCreateInstance (signature, this, message);
45+
}
3246
javaStackTrace = _GetJavaStack (SafeHandle);
3347
}
3448

3549
public JavaException (JniReferenceSafeHandle handle, JniHandleOwnership transfer)
3650
: base (_GetMessage (handle), _GetCause (handle))
3751
{
38-
JavaVM.SetObjectSafeHandle (this, handle, transfer);
52+
if (handle == null)
53+
return;
54+
using (SetSafeHandle (handle, transfer)) {
55+
}
3956
javaStackTrace = _GetJavaStack (SafeHandle);
4057
}
4158

@@ -65,6 +82,15 @@ public override string StackTrace {
6582
}
6683
}
6784

85+
protected SetSafeHandleCompletion SetSafeHandle (JniReferenceSafeHandle handle, JniHandleOwnership transfer)
86+
{
87+
return JniEnvironment.Current.JavaVM.SetObjectSafeHandle (
88+
this,
89+
handle,
90+
transfer,
91+
a => new SetSafeHandleCompletion (a));
92+
}
93+
6894
public void RegisterWithVM ()
6995
{
7096
JniEnvironment.Current.JavaVM.RegisterObject (this);
@@ -138,24 +164,6 @@ string _GetJavaStack (JniReferenceSafeHandle handle)
138164
}
139165
}
140166

141-
static JniLocalReference _NewObject (Type type, JniPeerMembers peerMembers)
142-
{
143-
var info = JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (type);
144-
if (info.JniTypeName == null)
145-
throw new NotSupportedException (
146-
string.Format ("Cannot create instance of type '{0}': no Java peer type found.",
147-
type.FullName));
148-
149-
if (type == peerMembers.ManagedPeerType) {
150-
var c = peerMembers.InstanceMethods.GetConstructor ("()V");
151-
return peerMembers.JniPeerType.NewObject (c);
152-
}
153-
using (var t = new JniType (info.ToString ()))
154-
using (var c = t.GetConstructor ("()V")) {
155-
return t.NewObject (c);
156-
}
157-
}
158-
159167
int IJavaObjectEx.IdentityHashCode {
160168
get {return identity;}
161169
set {identity = value;}
@@ -175,6 +183,22 @@ void IJavaObjectEx.SetSafeHandle (JniReferenceSafeHandle handle)
175183
{
176184
SafeHandle = handle;
177185
}
186+
187+
protected struct SetSafeHandleCompletion : IDisposable {
188+
189+
readonly Action action;
190+
191+
public SetSafeHandleCompletion (Action action)
192+
{
193+
this.action = action;
194+
}
195+
196+
public void Dispose ()
197+
{
198+
if (action != null)
199+
action ();
200+
}
201+
}
178202
}
179203
}
180204

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

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,30 +26,28 @@ public virtual JniPeerMembers JniPeerMembers {
2626

2727
public JavaObject (JniReferenceSafeHandle handle, JniHandleOwnership transfer)
2828
{
29-
JavaVM.SetObjectSafeHandle (this, handle, transfer);
29+
if (handle == null)
30+
return;
31+
using (SetSafeHandle (handle, transfer)) {
32+
}
3033
}
3134

32-
static JniLocalReference _NewObject (Type type, JniPeerMembers peerMembers)
35+
public JavaObject ()
3336
{
34-
var info = JniEnvironment.Current.JavaVM.GetJniTypeInfoForType (type);
35-
if (info.JniTypeName == null)
36-
throw new NotSupportedException (
37-
string.Format ("Cannot create instance of type '{0}': no Java peer type found.",
38-
type.FullName));
39-
40-
if (type == peerMembers.ManagedPeerType) {
41-
var c = peerMembers.InstanceMethods.GetConstructor ("()V");
42-
return peerMembers.JniPeerType.NewObject (c);
43-
}
44-
using (var t = new JniType (info.ToString ()))
45-
using (var c = t.GetConstructor ("()V")) {
46-
return t.NewObject (c);
37+
using (SetSafeHandle (
38+
JniPeerMembers.InstanceMethods.StartCreateInstance ("()V", GetType ()),
39+
JniHandleOwnership.Transfer)) {
40+
JniPeerMembers.InstanceMethods.FinishCreateInstance ("()V", this);
4741
}
4842
}
4943

50-
public JavaObject ()
44+
protected SetSafeHandleCompletion SetSafeHandle (JniReferenceSafeHandle handle, JniHandleOwnership transfer)
5145
{
52-
JavaVM.SetObjectSafeHandle (this, _NewObject (GetType (), JniPeerMembers), JniHandleOwnership.Transfer);
46+
return JniEnvironment.Current.JavaVM.SetObjectSafeHandle (
47+
this,
48+
handle,
49+
transfer,
50+
a => new SetSafeHandleCompletion (a));
5351
}
5452

5553
public void RegisterWithVM ()
@@ -117,6 +115,22 @@ void IJavaObjectEx.SetSafeHandle (JniReferenceSafeHandle handle)
117115
{
118116
SafeHandle = handle;
119117
}
118+
119+
protected struct SetSafeHandleCompletion : IDisposable {
120+
121+
readonly Action action;
122+
123+
public SetSafeHandleCompletion (Action action)
124+
{
125+
this.action = action;
126+
}
127+
128+
public void Dispose ()
129+
{
130+
if (action != null)
131+
action ();
132+
}
133+
}
120134
}
121135
}
122136

src/Java.Interop/Java.Interop/JavaVM.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ public class JavaVMOptions {
8383
public bool TrackIDs {get; set;}
8484
public bool DestroyVMOnDispose {get; set;}
8585

86+
// Prefer JNIEnv::NewObject() over JNIEnv::AllocObject() + JNIEnv::CallNonvirtualVoidMethod()
87+
public bool NewObjectRequired {get; set;}
88+
8689
public JavaVMSafeHandle VMHandle {get; set;}
8790
public JniEnvironmentSafeHandle EnvironmentHandle {get; set;}
8891

@@ -146,6 +149,8 @@ public static void SetCurrent (JavaVM newCurrent)
146149

147150
public JavaVMSafeHandle SafeHandle {get; private set;}
148151

152+
public bool NewObjectRequired {get; private set;}
153+
149154
protected JavaVM (JavaVMOptions options)
150155
{
151156
if (options == null)
@@ -158,6 +163,8 @@ protected JavaVM (JavaVMOptions options)
158163
TrackIDs = options.TrackIDs;
159164
DestroyVM = options.DestroyVMOnDispose;
160165

166+
NewObjectRequired = options.NewObjectRequired;
167+
161168
SafeHandle = options.VMHandle;
162169
Invoker = SafeHandle.CreateInvoker ();
163170

@@ -378,21 +385,36 @@ internal void UnRegisterObject (IJavaObjectEx value)
378385
(t = (IJavaObject) wv.Target) != null &&
379386
object.ReferenceEquals (value, t))
380387
RegisteredInstances.Remove (key);
388+
value.Registered = false;
381389
}
382390
}
383391

384-
internal static void SetObjectSafeHandle<T> (T value, JniReferenceSafeHandle handle, JniHandleOwnership transfer)
392+
internal TCleanup SetObjectSafeHandle<T, TCleanup> (T value, JniReferenceSafeHandle handle, JniHandleOwnership transfer, Func<Action, TCleanup> createCleanup)
385393
where T : IJavaObject, IJavaObjectEx
394+
where TCleanup : IDisposable
386395
{
387396
if (handle == null)
388397
throw new ArgumentNullException ("handle");
389398
if (handle.IsInvalid)
390399
throw new ArgumentException ("handle is invalid.", "handle");
391400

401+
bool register = handle is JniAllocObjectRef;
402+
392403
value.SetSafeHandle (handle.NewLocalRef ());
393404
JniEnvironment.Handles.Dispose (handle, transfer);
394405

395406
value.IdentityHashCode = JniSystem.IdentityHashCode (value.SafeHandle);
407+
408+
if (register) {
409+
RegisterObject (value);
410+
Action unregister = () => {
411+
UnRegisterObject (value);
412+
using (var g = value.SafeHandle)
413+
value.SetSafeHandle (g.NewLocalRef ());
414+
};
415+
return createCleanup (unregister);
416+
}
417+
return createCleanup (null);
396418
}
397419

398420
internal void DisposeObject<T> (T value)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace Java.Interop {
4+
5+
class JniAllocObjectRef : JniLocalReference
6+
{
7+
public JniAllocObjectRef (IntPtr handle)
8+
{
9+
SetHandle (handle);
10+
}
11+
}
12+
}
13+

src/Java.Interop/Java.Interop/JniLocalReference.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,12 @@ internal IntPtr ReturnToJniRef ()
2727
JniEnvironment.Current.LogDestroyLocalRef (h);
2828
return h;
2929
}
30+
31+
internal JniAllocObjectRef ToAllocObjectRef ()
32+
{
33+
var h = handle;
34+
handle = IntPtr.Zero;
35+
return new JniAllocObjectRef (h);
36+
}
3037
}
3138
}

src/Java.Interop/Java.Interop/JniPeerInstanceMethods.cs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,99 @@ public sealed partial class JniPeerInstanceMethods
77
{
88
internal JniPeerInstanceMethods (JniPeerMembers members)
99
{
10-
Members = members;
10+
DeclaringType = members.ManagedPeerType;
11+
JniPeerType = members.JniPeerType;
1112
}
1213

13-
readonly JniPeerMembers Members;
14+
JniPeerInstanceMethods (Type declaringType)
15+
{
16+
var jvm = JniEnvironment.Current.JavaVM;
17+
var info = jvm.GetJniTypeInfoForType (declaringType);
18+
if (info.JniTypeName == null)
19+
throw new NotSupportedException (
20+
string.Format ("Cannot create instance of type '{0}': no Java peer type found.",
21+
declaringType.FullName));
22+
23+
DeclaringType = declaringType;
24+
JniPeerType = new JniType (info.ToString ());
25+
JniPeerType.RegisterWithVM ();
26+
}
27+
28+
readonly Type DeclaringType;
29+
readonly JniType JniPeerType;
1430
readonly Dictionary<string, JniInstanceMethodID> InstanceMethods = new Dictionary<string, JniInstanceMethodID>();
31+
readonly Dictionary<Type, JniPeerInstanceMethods> SubclassConstructors = new Dictionary<Type, JniPeerInstanceMethods> ();
1532

1633
public JniInstanceMethodID GetConstructor (string signature)
1734
{
18-
string method = "<init>";
35+
if (signature == null)
36+
throw new ArgumentNullException ("signature");
1937
lock (InstanceMethods) {
2038
JniInstanceMethodID m;
2139
if (!InstanceMethods.TryGetValue (signature, out m)) {
22-
m = Members.JniPeerType.GetInstanceMethod (method, signature);
40+
m = JniPeerType.GetConstructor (signature);
2341
InstanceMethods.Add (signature, m);
2442
}
2543
return m;
2644
}
2745
}
2846

47+
JniPeerInstanceMethods GetConstructorsForType (Type declaringType)
48+
{
49+
if (declaringType == DeclaringType)
50+
return this;
51+
52+
JniPeerInstanceMethods methods;
53+
lock (SubclassConstructors) {
54+
if (!SubclassConstructors.TryGetValue (declaringType, out methods)) {
55+
methods = new JniPeerInstanceMethods (declaringType);
56+
SubclassConstructors.Add (declaringType, methods);
57+
}
58+
}
59+
return methods;
60+
}
61+
2962
public JniInstanceMethodID GetMethodID (string encodedMember)
3063
{
3164
lock (InstanceMethods) {
3265
JniInstanceMethodID m;
3366
if (!InstanceMethods.TryGetValue (encodedMember, out m)) {
3467
string method, signature;
3568
JniPeerMembers.GetNameAndSignature (encodedMember, out method, out signature);
36-
m = Members.JniPeerType.GetInstanceMethod (method, signature);
69+
m = JniPeerType.GetInstanceMethod (method, signature);
3770
InstanceMethods.Add (encodedMember, m);
3871
}
3972
return m;
4073
}
4174
}
75+
76+
public JniLocalReference StartCreateInstance (string constructorSignature, Type declaringType, params JValue[] arguments)
77+
{
78+
if (JniEnvironment.Current.JavaVM.NewObjectRequired) {
79+
return NewObject (constructorSignature, declaringType, arguments);
80+
}
81+
using (var lref = GetConstructorsForType (declaringType)
82+
.JniPeerType
83+
.AllocObject ())
84+
return lref.ToAllocObjectRef ();
85+
}
86+
87+
JniLocalReference NewObject (string constructorSignature, Type declaringType, JValue[] arguments)
88+
{
89+
var methods = GetConstructorsForType (declaringType);
90+
var ctor = methods.GetConstructor (constructorSignature);
91+
return methods.JniPeerType.NewObject (ctor, arguments);
92+
}
93+
94+
public void FinishCreateInstance (string constructorSignature, IJavaObject self, params JValue[] arguments)
95+
{
96+
if (JniEnvironment.Current.JavaVM.NewObjectRequired) {
97+
return;
98+
}
99+
var methods = GetConstructorsForType (self.GetType ());
100+
var ctor = methods.GetConstructor (constructorSignature);
101+
ctor.CallNonvirtualVoidMethod (self.SafeHandle, methods.JniPeerType.SafeHandle, arguments);
102+
}
42103
}
43104

44105
struct JniArgumentMarshalInfo<T> {

0 commit comments

Comments
 (0)