Skip to content

Commit

Permalink
[Java.Interop] Allow JniRuntime init from JavaVM* and JNIEnv* (#1158)
Browse files Browse the repository at this point in the history
Context: #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
  • Loading branch information
jonpryor authored Nov 9, 2023
1 parent 38c8a82 commit 28849ec
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 10 deletions.
31 changes: 28 additions & 3 deletions src/Java.Interop/Java.Interop/JniRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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> (T value)
where T : class, ISetRuntime
{
Expand Down
22 changes: 15 additions & 7 deletions src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ public class JreRuntimeOptions : JniRuntime.CreationOptions {
public JreRuntimeOptions ()
{
JniVersion = JniVersion.v1_2;
ClassPath = new Collection<string> () {
Path.Combine (
Path.GetDirectoryName (typeof (JreRuntimeOptions).Assembly.Location) ?? throw new NotSupportedException (),
"java-interop.jar"),
};
ClassPath = new Collection<string> ();
}

public JreRuntimeOptions AddOption (string option)
Expand Down Expand Up @@ -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 ();
Expand All @@ -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 FileNotFoundException ($"`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,
Expand Down
19 changes: 19 additions & 0 deletions tests/Java.Interop-Tests/Java.Interop/JniRuntimeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 28849ec

Please sign in to comment.