Skip to content

Commit c8f3e51

Browse files
committed
[Java.Interop] Java native method registration
The Java Native Interface Functions [0] mostly covers calling Java code *from* non-Java code, e.g. JNIEnv::CallVoidMethod() can be used to call all Java instance methods which have a `void` return type. What about the calling non-Java code from Java? That can't be *fully* done through the JNIEnv type; it also requires cooperation with Java bytecode, though the use of `native` method declarations [1]: // Java class Example { public native void m (); } Before Java code can invoke the Example.m() instance method, that method must first be *registered* via JNIEnv::RegisterNatives() [2]. What we have, then, is a two step process: 1. Write/generate/otherwise obtain Java bytecode with `native` method declarations. 2. At runtime, "somehow" ensure that the native methods declared in (1) are *registered* before they are first used from Java. To date, JNIEnv::RegisterNatives() has been used in numerous places in Java.Interop via JniType.RegisterNativeMethods() -- e.g. the JavaProxyObject static constructor -- but the current architecture doesn't support (2), as there is no way to *ensure* that e.g. JavaProxyObject registers its native methods before Java invokes them. (Normal use would preclude that, but if someone uses Reflection to create a JavaProxyObject instance...) How do we ensure that "something" -- native method registration -- happens before anything important is done with the Java type? By using Java static initalizers, and introducing the new com.xamarin.java_interop.ManagedPeer.registerNativeMembers() method: class Example { static final String assemblyQualifiedName = "Example, ..."; static { com.xamarin.java_interop.ManagedPeer.registerNativeMembers ( Example.class, assemblyQualifiedName, "method-blob" ); } } ManagedPeer.registerNativeMembers() will call into managed code and (eventually) call JNIEnv::RegisterNatives() for the Java type. Next important question: How does ManagedPeer.registerNativeMembers() *actually* register the native methods? Additionally, this mechanism needs to help support the future desired Ahead-Of-Time (AOT) compilation mechanism of jnimarshalmethod-gen (176240d). The answer is the new JniRuntime.JniTypeManager.RegisterNativeMembers() virtual method. The default implementation will do the following: 1. Lookup the assembly to use. 1.a: Check to see if there is a corresponding -JniMarshalMethods assembly, as generated by jnimarshalmethod-gen. For example, if the type to register is in Example.dll, then the assembly Example-JniMarshalMethods.dll will be loaded. 1.b: If (1.a) fails, then use the assembly that contains the Type being registered -- the assemblyQualifiedName parameter provided to ManagedPeer.registerNativeMembers(). 2. Lookup the type to use, which is the type's FullName resolved from the assembly found in (1). 3. Look for the __RegisterNativeMembers() method on the type from (2) and invoke it, passing the JniType which corresponds to the Java class to register, and the "method-blob" value. For example, when registering methods for the Example type in Example.dll, the following methods will be searched for, and the first method found will be invoked: Example.__RegisterNativeMembers() from Example-JniMarshalMethods.dll Example.__RegisterNativeMembers() from Example.dll If no such __RegisterNativeMembers() method is found, we bail. JniRuntime.JniTypeManager subclasses may do something else. For example, Xamairn.Android could (will?) process the "method-blob" string as it normally does within JNIEnv.RegisterJniNatives(). The benefit of this architecture is that it allows "normal user" assemblies to be post-processed to emit the -JniMarshalMethods assembly, and use *that* for method registration at runtime. This in turn avoids the need to emit marshaling code at runtime, as marshaling code was previously emitted. [0]: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html [1]: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#compiling_loading_and_linking_native_methods [2]: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives
1 parent fcfa22b commit c8f3e51

File tree

17 files changed

+224
-15
lines changed

17 files changed

+224
-15
lines changed

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,4 @@ run-android: $(ATESTS)
119119
run-test-jnimarshal: bin/$(CONFIGURATION)/Java.Interop.Export-Tests.dll
120120
MONO_TRACE_LISTENER=Console.Out \
121121
mono --debug bin/$(CONFIGURATION)/jnimarshalmethod-gen.exe bin/$(CONFIGURATION)/Java.Interop.Export-Tests.dll
122+
$(call RUN_TEST,$<)

src/Java.Interop.Export/Tests/Export-Tests.projitems

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</ItemGroup>
1818
<Target Name="BuildExportTestJar" Inputs="@(JavaExportTestJar)" Outputs="$(OutputPath)export-test.jar">
1919
<MakeDir Directories="$(OutputPath)et-classes" />
20-
<Exec Command="javac -source 1.5 -target 1.6 -d &quot;$(OutputPath)et-classes&quot; @(JavaExportTestJar -&gt; '%(Identity)', ' ')" />
20+
<Exec Command="javac -classpath &quot;$(OutputPath)java-interop.jar&quot; -source 1.5 -target 1.6 -d &quot;$(OutputPath)et-classes&quot; @(JavaExportTestJar -&gt; '%(Identity)', ' ')" />
2121
<Exec Command="jar cf &quot;$(OutputPath)export-test.jar&quot; -C &quot;$(OutputPath)et-classes&quot; ." />
2222
</Target>
2323
</Project>

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

+8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using System.Linq.Expressions;
34
using System.Reflection;
45

@@ -10,6 +11,13 @@ namespace Java.InteropTests
1011
[JniTypeSignature ("com/xamarin/interop/export/ExportType")]
1112
public class ExportTest : JavaObject
1213
{
14+
static void __RegisterNativeMembers (JniType type, string members)
15+
{
16+
var methods = JniEnvironment.Runtime.ExportedMemberBuilder
17+
.GetExportedMemberRegistrations (typeof (ExportTest));
18+
type.RegisterNativeMethods (methods.ToArray ());
19+
}
20+
1321
public ExportTest (ref JniObjectReference reference, JniObjectReferenceOptions transfer)
1422
: base (ref reference, transfer)
1523
{

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

-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ public void AddExportMethods ()
3333
Assert.AreEqual ("()V", methods [1].Signature);
3434
Assert.IsTrue (methods [1].Marshaler is Action<IntPtr, IntPtr>);
3535

36-
t.RegisterNativeMethods (methods.ToArray ());
37-
3836
var m = t.GetStaticMethod ("testStaticMethods", "()V");
3937
JniEnvironment.StaticMethods.CallStaticVoidMethod (t.PeerReference, m);
4038
Assert.IsTrue (ExportTest.StaticHelloCalled);

src/Java.Interop.Export/Tests/java/com/xamarin/interop/export/ExportType.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
package com.xamarin.interop.export;
22

3-
public class ExportType {
3+
import java.util.ArrayList;
4+
5+
import com.xamarin.java_interop.GCUserPeerable;
6+
7+
public class ExportType
8+
implements GCUserPeerable
9+
{
10+
11+
static final String assemblyQualifiedName = "Java.InteropTests.ExportTest, Java.Interop.Export-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
12+
static {
13+
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
14+
ExportType.class,
15+
assemblyQualifiedName,
16+
"");
17+
}
418

519
public static void testStaticMethods () {
620
staticAction ();
@@ -30,4 +44,16 @@ public void testMethods () {
3044
public native void action ();
3145
public native long funcInt64 ();
3246
public native Object funcIJavaObject ();
47+
48+
ArrayList<Object> managedReferences = new ArrayList<Object>();
49+
50+
public void jiAddManagedReference (java.lang.Object obj)
51+
{
52+
managedReferences.add (obj);
53+
}
54+
55+
public void jiClearManagedReferences ()
56+
{
57+
managedReferences.clear ();
58+
}
3359
}

src/Java.Interop/Java.Interop/JavaProxyObject.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sealed class JavaProxyObject : JavaObject
1111
static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject));
1212
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();
1313

14-
static JavaProxyObject ()
14+
static void __RegisterNativeMembers (JniType type, string members)
1515
{
1616
_members.JniPeerType.RegisterNativeMethods (
1717
new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", (Func<IntPtr, IntPtr, IntPtr, bool>) _Equals),

src/Java.Interop/Java.Interop/JavaProxyThrowable.cs

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ class JavaProxyThrowable : JavaException
77
{
88
new internal const string JniTypeName = "com/xamarin/java_interop/internal/JavaProxyThrowable";
99

10+
static void __RegisterNativeMembers (JniType type, string members)
11+
{
12+
}
13+
1014
public Exception Exception {get; private set;}
1115

1216
public JavaProxyThrowable (Exception exception)

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

+45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
34
using System.Linq;
45
using System.Reflection;
56

@@ -174,6 +175,50 @@ IEnumerable<Type> CreateGetTypesForSimpleReferenceEnumerator (string jniSimpleRe
174175
#endif // !XA_INTEGRATION
175176
yield break;
176177
}
178+
179+
public virtual void RegisterNativeMembers (JniType nativeClass, Type type, string methods)
180+
{
181+
if (!TryLoadExternalJniMarshalMethods (nativeClass, type, methods) &&
182+
!TryRegisterNativeMembers (nativeClass, type, methods)) {
183+
throw new NotSupportedException ($"Could not register Java.class={nativeClass.Name} Managed.type={type.FullName}");
184+
}
185+
}
186+
187+
static bool TryLoadExternalJniMarshalMethods (JniType nativeClass, Type type, string methods)
188+
{
189+
var marshalMethodAssemblyName = new AssemblyName (type.GetTypeInfo ().Assembly.GetName ().Name + "-JniMarshalMethods");
190+
var marshalMethodsAssembly = TryLoadAssembly (marshalMethodAssemblyName);
191+
if (marshalMethodsAssembly == null)
192+
return false;
193+
194+
var marshalType = marshalMethodsAssembly.GetType (type.FullName);
195+
if (marshalType == null)
196+
return false;
197+
return TryRegisterNativeMembers (nativeClass, marshalType, methods);
198+
}
199+
200+
static Assembly TryLoadAssembly (AssemblyName name)
201+
{
202+
try {
203+
return Assembly.Load (name);
204+
}
205+
catch (Exception e) {
206+
Debug.WriteLine ("Warning: Could not load JNI Marshal Method assembly '{0}': {1}", name, e);
207+
return null;
208+
}
209+
}
210+
211+
static bool TryRegisterNativeMembers (JniType nativeClass, Type marshalType, string methods)
212+
{
213+
var registerMethod = marshalType.GetTypeInfo ().GetDeclaredMethod ("__RegisterNativeMembers");
214+
if (registerMethod == null) {
215+
return false;
216+
}
217+
218+
var register = (Action<JniType, string>) registerMethod.CreateDelegate (typeof(Action<JniType, string>));
219+
register (nativeClass, methods);
220+
return true;
221+
}
177222
}
178223
}
179224
}

src/Java.Interop/Java.Interop/ManagedPeer.cs

+39-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ namespace Java.Interop {
2020
static ManagedPeer ()
2121
{
2222
_members.JniPeerType.RegisterNativeMethods (
23-
new JniNativeMethodRegistration ("construct", ConstructSignature, (Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) Construct)
23+
new JniNativeMethodRegistration (
24+
"construct",
25+
ConstructSignature,
26+
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) Construct),
27+
new JniNativeMethodRegistration (
28+
"registerNativeMembers",
29+
RegisterNativeMembersSignature,
30+
(Action<IntPtr, IntPtr, IntPtr, IntPtr, IntPtr>) RegisterNativeMembers)
2431
);
2532
}
2633

@@ -30,7 +37,7 @@ static ManagedPeer ()
3037

3138
internal static void Init ()
3239
{
33-
// Present so that JavaVM has _something_ to reference to
40+
// Present so that JniRuntime has _something_ to reference to
3441
// prompt invocation of the static constructor & registration
3542
}
3643

@@ -170,6 +177,36 @@ static object[] GetValues (JniRuntime runtime, JniObjectReference values, Type[]
170177

171178
return pvalues;
172179
}
180+
181+
const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V";
182+
183+
static void RegisterNativeMembers (
184+
IntPtr jnienv,
185+
IntPtr klass,
186+
IntPtr n_nativeClass,
187+
IntPtr n_assemblyQualifiedName,
188+
IntPtr n_methods)
189+
{
190+
var envp = new JniTransition (jnienv);
191+
try {
192+
var r_nativeClass = new JniObjectReference (n_nativeClass);
193+
var nativeClass = new JniType (ref r_nativeClass, JniObjectReferenceOptions.Copy);
194+
195+
var assemblyQualifiedName = JniEnvironment.Strings.ToString (new JniObjectReference (n_assemblyQualifiedName));
196+
var methods = JniEnvironment.Strings.ToString (new JniObjectReference (n_methods));
197+
198+
var type = Type.GetType (assemblyQualifiedName, throwOnError: true);
199+
200+
JniEnvironment.Runtime.TypeManager.RegisterNativeMembers (nativeClass, type, methods);
201+
}
202+
catch (Exception e) {
203+
Debug.WriteLine (e.ToString ());
204+
envp.SetPendingException (e);
205+
}
206+
finally {
207+
envp.Dispose ();
208+
}
209+
}
173210
}
174211

175212
class JniLocationException : Exception {

src/Java.Interop/Tests/Java.Interop/CallVirtualFromConstructorDerived.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase
99
new internal const string JniTypeName = "com/xamarin/interop/CallVirtualFromConstructorDerived";
1010
static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorDerived));
1111

12-
static CallVirtualFromConstructorDerived ()
12+
static void __RegisterNativeMembers (JniType type, string members)
1313
{
1414
_members.JniPeerType.RegisterNativeMethods (
1515
new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (Action<IntPtr, IntPtr, int>) CalledFromConstructorHandler));

src/Java.Interop/Tests/Java.Interop/TestType.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public partial class TestType : JavaObject
1414
internal const string JniTypeName = "com/xamarin/interop/TestType";
1515
static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (TestType));
1616

17-
static TestType ()
17+
static void __RegisterNativeMembers (JniType type, string members)
1818
{
1919
_members.JniPeerType.RegisterNativeMethods (
2020
new JniNativeMethodRegistration ("equalsThis", "(Ljava/lang/Object;)Z", GetEqualsThisHandler ()),

src/Java.Interop/Tests/java/com/xamarin/interop/CallVirtualFromConstructorDerived.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ public class CallVirtualFromConstructorDerived
88
extends CallVirtualFromConstructorBase
99
implements GCUserPeerable
1010
{
11+
static final String assemblyQualifiedName = "Java.InteropTests.CallVirtualFromConstructorDerived, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
12+
static {
13+
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
14+
CallVirtualFromConstructorDerived.class,
15+
assemblyQualifiedName,
16+
"");
17+
}
18+
1119
ArrayList<Object> managedReferences = new ArrayList<Object>();
1220

1321
public CallVirtualFromConstructorDerived (int value) {
1422
super (value);
1523
if (CallVirtualFromConstructorDerived.class == getClass ()) {
1624
com.xamarin.java_interop.ManagedPeer.construct (
1725
this,
18-
"Java.InteropTests.CallVirtualFromConstructorDerived, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
26+
assemblyQualifiedName,
1927
"System.Int32",
2028
value
2129
);

src/Java.Interop/Tests/java/com/xamarin/interop/TestType.java

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@
66

77
public class TestType implements GCUserPeerable {
88

9+
static final String assemblyQualifiedName = "Java.InteropTests.TestType, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null";
10+
static {
11+
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
12+
TestType.class,
13+
assemblyQualifiedName,
14+
"");
15+
}
16+
917
ArrayList<Object> managedReferences = new ArrayList<Object>();
1018

1119
public TestType () {
1220
if (TestType.class == getClass ()) {
1321
com.xamarin.java_interop.ManagedPeer.construct (
1422
this,
15-
"Java.InteropTests.TestType, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
23+
assemblyQualifiedName,
1624
""
1725
);
1826
}
@@ -23,8 +31,8 @@ public TestType (TestType a, int b) {
2331
if (TestType.class == getClass ()) {
2432
com.xamarin.java_interop.ManagedPeer.construct (
2533
this,
26-
"Java.InteropTests.TestType, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null",
27-
"Java.InteropTests.TestType, Java.Interop-Tests, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null:System.Int32",
34+
assemblyQualifiedName,
35+
assemblyQualifiedName + ":System.Int32",
2836
a, b
2937
);
3038
}

src/Java.Interop/java/com/xamarin/java_interop/ManagedPeer.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
private ManagedPeer () {
55
}
66

7-
// public static native void registerNativeMethods (java.lang.Class<?> nativeClass, String managedType, String methods);
7+
public static native void registerNativeMembers (
8+
java.lang.Class<?> nativeClass,
9+
String assemblyQualifiedName,
10+
String methods);
811

912
public static native void construct (
1013
Object self,

src/Java.Interop/java/com/xamarin/java_interop/internal/JavaProxyObject.java

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
extends java.lang.Object
99
implements GCUserPeerable
1010
{
11+
static final String assemblyQualifiedName = "Java.Interop.JavaProxyObject, Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null";
12+
static {
13+
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
14+
JavaProxyObject.class,
15+
assemblyQualifiedName,
16+
"");
17+
}
18+
1119
ArrayList<Object> managedReferences = new ArrayList<Object>();
1220

1321
@Override

src/Java.Interop/java/com/xamarin/java_interop/internal/JavaProxyThrowable.java

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
extends java.lang.Throwable
99
implements GCUserPeerable
1010
{
11+
static final String assemblyQualifiedName = "Java.Interop.JavaProxyThrowable, Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null";
12+
static {
13+
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
14+
JavaProxyObject.class,
15+
assemblyQualifiedName,
16+
"");
17+
}
18+
1119
ArrayList<Object> managedReferences = new ArrayList<Object>();
1220

1321
public JavaProxyThrowable () {

0 commit comments

Comments
 (0)