Closed iamcalledrob closed 2 years ago
For the second scenario, i think we do want to have the Go code loaded as a shared library. We don't need to use JNI_OnLoad
though. In the Java code we can define a method as native, which is implemented in the Go shared library and call this method from the Java code. A JNIEnv pointer is passed as the first argument to the implementing function in the library. JNI docs for this
So there is a defined entry point to the Go code with the JVM already started. The Go runtime it self is started when the library is first loaded.
I've done this before in a Java project on Linux, I can post some code on how I did this. So I'm hoping this would work on Android too. It maybe worth implementing something in JNIGI as a default way of setting this up.
For the version constants, that is a good change, i think we could just redefine these in Go and not use the C definitions.
I took a stab at implementing both scenarios (1) and (2), but came to the realisation that (1) probably isn't very useful, because you can't access Android Application APIs (anything that requires a Context). It would only be useful for Android command-line tools which want to interface with a non-Android JVM library. It is possible, however.
For (2), you're right, a native function could be defined, which would give access to then VM/Env when called. However I believe this function would need to be in the "main" package that's compiled to a shared library. This would essentially provide the same functionality as JNI_OnLoad.
For my own use-cases, it'd more convenient to use JNI_OnLoad, because this can happen once, automatically, without any manual bootstrapping. It's then possible to write packages which import jnigi that don't need to be initialized with a VM/Env.
Put up a quick PR that handles scenario (2) here: https://github.com/timob/jnigi/pull/56
I've also noticed a couple of issues on Android with exception handling:
(1) env->ExceptionDescribe()
logs to logcat on Android, since stdout/stderr are routed to /dev/null. However if a subsequent panic()
occurs, the logcat write never occurs. I'd assume a buffer needs to be flushed, but I don't see any system APIs to do so. This leads to Java exception occured. check stderr
being returned, but nothing in stderr/logcat.
(2) Changing the exception handler to ThrowableToStringExceptionHandler
results in another exception whilst processing the prior exception: java.lang.NoSuchMethodError: no non-static method "Ljava/lang/String;.<init>(Ljava/lang/String;)[B"
.
This can be traced to the following func, which is called as part of constructing the exception error string:
func stringFromJavaLangString(env *Env, ref *ObjectRef) string {
if ref.IsNil() {
return ""
}
env.PrecalculateSignature("(Ljava/lang/String;)[B") // <--
var ret []byte
err := ref.CallMethod(env, "getBytes", &ret, env.GetUTF8String())
if err != nil {
return ""
}
return string(ret)
}
Removing the PrecalculateSignature call fixes the issue, as does calling env.GetUTF8String() before the first exception is thrown.
Updated PR for issue (2) above, I think it was an existing issue. env.GetUTF8String()
makes a method call, and was using the pre-calculated signature meant for ref.CallMethod(..., "getBytes")
, which was causing the issue.
After integrating this with a real project, I've refactored the integration to be similar to what you're suggesting—instead of working "magically" with JNI_OnLoad, instead jnigi can now be provided with the JNI environment via a new UseJVM()
function, which you'd call instead of CreateJVM()
.
There's nothing precluding someone from using the environment provided by JNI_OnLoad, but it no longer requires it. This also has the side-effect of removing all the Android-specific code that was related to JNI_OnLoad :)
I also added support for using an additional class loader, which is needed to find non-system classes outside of JNI_OnLoad when being used as a shared library.
This was pretty confusing to work through. There's some limited notes on this here, as well as scattered on StackOverflow and Google Groups. In particular:
Any FindClass calls made from JNI_OnLoad will resolve classes in the context of the class loader that was used to load the shared library. When called from other contexts, FindClass uses the class loader associated with the method at the top of the Java stack, or if there isn't one (because the call is from a native thread that was just attached) it uses the "system" class loader. The system class loader does not know about your application's classes, so you won't be able to look up your own classes with FindClass in that context.
The custom class loader can only be attached as part of UseJVM()
, since I'm assuming it wouldn't be necessary in cases where you are creating the JVM.
This all looks good! I've had a look at the PR looks like it will accomplish what we want. I'll test it and update.
So the object you pass to UseJVM()
will be an instance of a custom class, and it will use the class loader associated with this class which will be able to locate other custom classes. If I'm reading this right.
Doing both lookups for classes is great, and it's written in C :)
Merged the PR, thanks for this. I'll leave this issue open for now.
Thanks for looking into this!
So the object you pass to UseJVM() will be an instance of a custom class, and it will use the class loader associated with this class which will be able to locate other custom classes. If I'm reading this right.
That's right :)
Closing the issue since this is merged and working now.
@timob could you share the linux examples you mentioned? are you using WrapEnv in those examples?
I'm interested in building a shared library using go to be used in a java project.
I tried passing env *C.JNIEnv
to WrapEnv
but the compiler says the types don't agree (*_Ctype_JNIEnv and unsafe.Pointer). I feel I'm going in the wrong direction trying to use WrapEnv... but idk,
A primary goal is to return custom java objects from the go native functions. For this I need access to the JVM and a class loader. sounds like UseJVM() is the correct path. any examples of this?
I'm exploring adding Android support to this library, and have a few questions/thoughts.
Initialization
Context: There are two ways to get a reference to the JVM in Android, depending on how the process is set up.
JNI_CreateJavaVM
from libart.soJNI_OnLoad
will be called with the corresponding JVM/Env. It's not possible to useJNI_CreateJavaVM
The initialization patterns of this library currently map 1:1 to (1) above. I could easily create an
android.go
implementation that implementsLoadJVMLib
andjni_CreateJavaVM
. But I imagine most people will want to use this in scenario (2)—where their Go code is acting as a shared library.I'm curious how to think about initialization for scenario (2). My current idea would be to implement
LoadJVMLib
andjni_CreateJavaVM
, which would plug into the existing code.Then have a new higher level constructor, e.g.
FindOrCreateJVM()
which either callsCreateJVM
or creates a new*JVM, *Env
pair from the JVM provided by the host environment ifJNI_OnLoad
was invoked by the JVM already. This would probably need to be paired with changes toDestroy()
so that it only does so if the JVM was actually created by us.Constants
This causes a compile failure when building with the Android NDK, which only supports up to JNI_VERSION_1_6. It would be possible to shim this so that JNI_VERSION_1_8 exists in C-land, though you wouldn't actually be able to use it.
--
In theory, this should be all it takes to get this working on Android too. Curious if you have any thoughts/advice?