From fc6f6792bddce6bff4b31bb71d3583c78b5aa517 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 9 Nov 2023 14:04:27 -0500 Subject: [PATCH 1/2] [Java.Interop] Allow JniRuntime init from JavaVM* and JNIEnv* Context: https://github.com/xamarin/java.interop/pull/1153 [JNI][0] supports *two* modes of operation: 1. Native code creates the JVM, e.g. via [`JNI_CreateJavaVM()`][1] 2. The JVM already exists, and when Java code calls [`System.loadLibrary()`][3], the JVM calls the [`JNI_OnLoad()`][2] function on the specified library. Java.Interop samples and unit tests rely on the first approach, e.g. `TestJVM` subclasses `JreRuntime`, which is responsible for calling `JNI_CreateJavaVM()` so that Java code can be used. PR #1153 is exploring the use of [.NET Native AOT][4] to produce a native library which is used with Java-originated initialization. In order to make Java-originated initialization *work*, we need to be able to initialize `JniRuntime` and `JreRuntime` around existing JVM-provided pointers: * The `JavaVM*` provided to `JNI_OnLoad()`, which can be used to set `JniRuntime.CreationOptions.InvocationPointer`: [UnmanagedCallersOnly(EntryPoint="JNI_OnLoad")] int JNI_OnLoad(IntPtr vm, IntPtr reserved) { var options = new JreRuntimeOptions { InvocationPointer = vm, }; var runtime = options.CreateJreVM (); return runtime.JniVersion; return JNI_VERSION_1_6; } * The [`JNIEnv*` value provided to Java `native` methods][5] when they are invoked, which can be used to set `JniRuntime.CreationOptions.EnvironmentPointer`: [UnmanagedCallersOnly(EntryPoint="Java_example_Whatever_init")] void Whatever_init(IntPtr jnienv, IntPtr Whatever_class) { var options = new JreRuntimeOptions { EnvironmentPointer = jnienv, }; var runtime = options.CreateJreVM (); } Update `JniRuntime` and `JreRuntime` to support these Java-originated initialization strategies. In particular, don't require that `JreRuntimeOptions.JvmLibraryPath` be set, avoiding: System.InvalidOperationException: Member `JreRuntimeOptions.JvmLibraryPath` must be set. at Java.Interop.JreRuntime.CreateJreVM(JreRuntimeOptions builder) at Java.Interop.JreRuntime..ctor(JreRuntimeOptions builder) at Java.Interop.JreRuntimeOptions.CreateJreVM() [0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html [1]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#creating_the_vm [2]: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Runtime.html#loadLibrary(java.lang.String) [3]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/invocation.html#JNJI_OnLoad [4]: https://learn.microsoft.com/dotnet/core/deploying/native-aot/ [5]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#native_method_arguments --- src/Java.Interop/Java.Interop/JniRuntime.cs | 31 +++++++++++++++++-- .../Java.Interop/JreRuntime.cs | 22 ++++++++----- .../Java.Interop/JniRuntimeTest.cs | 19 ++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniRuntime.cs b/src/Java.Interop/Java.Interop/JniRuntime.cs index cd377f3f2..75b1b4485 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.cs @@ -165,8 +165,8 @@ protected JniRuntime (CreationOptions options) { if (options == null) throw new ArgumentNullException (nameof (options)); - if (options.InvocationPointer == IntPtr.Zero) - throw new ArgumentException ("options.InvocationPointer is null", nameof (options)); + if (options.InvocationPointer == IntPtr.Zero && options.EnvironmentPointer == IntPtr.Zero) + throw new ArgumentException ("Need either options.InvocationPointer or options.EnvironmentPointer!", nameof (options)); TrackIDs = options.TrackIDs; DestroyRuntimeOnDispose = options.DestroyRuntimeOnDispose; @@ -175,7 +175,12 @@ protected JniRuntime (CreationOptions options) NewObjectRequired = options.NewObjectRequired; JniVersion = options.JniVersion; - InvocationPointer = options.InvocationPointer; + + if (options.InvocationPointer == IntPtr.Zero && options.EnvironmentPointer != IntPtr.Zero) { + InvocationPointer = GetInvocationPointerFromEnvironmentPointer (options.EnvironmentPointer); + } else { + InvocationPointer = options.InvocationPointer; + } Invoker = CreateInvoker (InvocationPointer); SetValueManager (options); @@ -230,6 +235,26 @@ protected JniRuntime (CreationOptions options) #endif // !XA_JI_EXCLUDE } + static unsafe IntPtr GetInvocationPointerFromEnvironmentPointer (IntPtr envp) + { + IntPtr vm = IntPtr.Zero; +#if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS + if (JniNativeMethods.GetJavaVM (envp, &vm) is int r && + r != JNI_OK) { + throw new InvalidOperationException ($"Could not obtain JavaVM* from JNIEnv*; JNIEnv::GetJavaVM() returned {r}!"); + } +#elif FEATURE_JNIENVIRONMENT_JI_PINVOKES + if (NativeMethods.java_interop_jnienv_get_java_vm (envp, out vm) is int r && + r != JNI_OK) { + throw new InvalidOperationException ($"Could not obtain JavaVM* from JNIEnv*; JNIEnv::GetJavaVM() returned {r}!"); + } +#else // !FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS && !FEATURE_JNIENVIRONMENT_JI_PINVOKES + throw new NotSupportedException ("Cannot obtain JavaVM* from JNIEnv*! " + + "Rebuild with FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS or FEATURE_JNIENVIRONMENT_JI_PINVOKES set!"); +#endif // !FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS && !FEATURE_JNIENVIRONMENT_JI_PINVOKES + return vm; + } + T SetRuntime (T value) where T : class, ISetRuntime { diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index aace91706..4d0d27a03 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -39,11 +39,7 @@ public class JreRuntimeOptions : JniRuntime.CreationOptions { public JreRuntimeOptions () { JniVersion = JniVersion.v1_2; - ClassPath = new Collection () { - Path.Combine ( - Path.GetDirectoryName (typeof (JreRuntimeOptions).Assembly.Location) ?? throw new NotSupportedException (), - "java-interop.jar"), - }; + ClassPath = new Collection (); } public JreRuntimeOptions AddOption (string option) @@ -80,7 +76,9 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) { if (builder == null) throw new ArgumentNullException ("builder"); - if (string.IsNullOrEmpty (builder.JvmLibraryPath)) + if (builder.InvocationPointer == IntPtr.Zero && + builder.EnvironmentPointer == IntPtr.Zero && + string.IsNullOrEmpty (builder.JvmLibraryPath)) throw new InvalidOperationException ($"Member `{nameof (JreRuntimeOptions)}.{nameof (JreRuntimeOptions.JvmLibraryPath)}` must be set."); builder.LibraryHandler = JvmLibraryHandler.Create (); @@ -99,11 +97,21 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new ManagedObjectReferenceManager (builder.JniGlobalReferenceLogWriter, builder.JniLocalReferenceLogWriter); } - if (builder.InvocationPointer != IntPtr.Zero) + if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) return builder; builder.LibraryHandler.LoadJvmLibrary (builder.JvmLibraryPath!); + if (!builder.ClassPath.Any (p => p.EndsWith ("java-interop.jar", StringComparison.OrdinalIgnoreCase))) { + var loc = typeof (JreRuntimeOptions).Assembly.Location; + var dir = string.IsNullOrEmpty (loc) ? null : Path.GetDirectoryName (loc); + var jij = string.IsNullOrEmpty (dir) ? null : Path.Combine (dir, "java-interop.jar"); + if (!File.Exists (jij)) { + throw new InvalidOperationException ($"`java-interop.jar` is required. Please add to `JreRuntimeOptions.ClassPath`. Tried to find it in `{jij}`."); + } + builder.ClassPath.Add (jij); + } + var args = new JavaVMInitArgs () { version = builder.JniVersion, nOptions = builder.Options.Count + 1, diff --git a/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs index f6f8c3858..865074ef6 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs @@ -36,6 +36,25 @@ public void JDK_OnlySupportsOneVM () Assert.Fail ("Expected NotSupportedException; got: {0}", e); } } + + [Test] + public void UseInvocationPointerOnNewThread () + { + var InvocationPointer = JniRuntime.CurrentRuntime.InvocationPointer; + + var t = new Thread (() => { + try { + var second = new JreRuntimeOptions () { + InvocationPointer = InvocationPointer, + }.CreateJreVM (); + } + catch (Exception e) { + Assert.Fail ("Expected no exception, got: {0}", e); + } + }); + t.Start (); + t.Join (); + } #endif // !__ANDROID__ [Test] From d4ea4b34e864db0e2e64ff077c4ef615f53c66ed Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 9 Nov 2023 15:05:47 -0500 Subject: [PATCH 2/2] Update JreRuntime.cs Throw FileNotFoundException. --- src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index 4d0d27a03..d7cc0f648 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -107,7 +107,7 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder) var dir = string.IsNullOrEmpty (loc) ? null : Path.GetDirectoryName (loc); var jij = string.IsNullOrEmpty (dir) ? null : Path.Combine (dir, "java-interop.jar"); if (!File.Exists (jij)) { - throw new InvalidOperationException ($"`java-interop.jar` is required. Please add to `JreRuntimeOptions.ClassPath`. Tried to find it in `{jij}`."); + throw new FileNotFoundException ($"`java-interop.jar` is required. Please add to `JreRuntimeOptions.ClassPath`. Tried to find it in `{jij}`."); } builder.ClassPath.Add (jij); }