You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Include some helpers that glue over the percularities of language-specific behavior in uniffi, specifically for the Kotlin/JVM target.
Background
I just recently moved a project at work over from plain JNI to using uniffi for a Rust/Android mixed project. It seemingly worked great at first, but it was only later that I discovered a few footguns here and there.
One of them was the attachment to the current thread for JVM targets, when spawning any kind of threads from the Rust side. In my case a multi-threaded Tokio runtime.
The docs about it were there, but not very prominently and I thought this could be something to be included directly into uniffi as utility functions to gloss over percularities of specific targets like JVM, Python, ...
Proposal
I propose to include some module into the uniffi crate, that would allow to easily attach the thread to the JVM which is what most people would want for better performance, I'd imagine.
This could be a function that is always present, regardless of the target, and only invoked when in a JVM environment.
Dependencies
I think it would be best to directly implement those APIs instead of relying on the jni crate. It brings lots of dependencies with it, and none of them are needed for the JNI APIs in question.
After some experimentation I now have a local minimal version that can attach the JVM to the current thread, only would need to copy it over to the uniffi crate.
Open questions
How to detect the target environment
The first thing that comes to mind are feature flags to enable a certain target environment. I'm using the cargo-ndk crate to build for Android and passing in a feature flag like --features uniffi/kotlin would probably be the easiest.
One danger is of course, when compiling with --all-features to compile for any other language it would at some point try to call some JNI functions that wouldn't be present.
An alternative could be to provide some extra config but invoking that in Cargo is more tedious.
Maybe there is a better way to do this?
Provide a target independent API or specific
Not sure what would be the best way. Either publicly expose those functions behind a feature flag or have an always present API that does its thing for Kotlin, but is a no-op for other languages. However, the latter feels more convenient.
For example:
// Assuming there is a `uniffi::kotlin` module, and the own crate has a `kotlin` feature flag.#[cfg(feature = "kotlin")]
uniffi::kotlin::attach_current_thread();
// Assuming there is an always present `uniffi::util` module. Does its thing for Kotlin, and no-op for other languages.
uniffi::util::attach();
How to retrieve the JavaVM instance
Basically there are three choices. In any case, the VM instance is global and can be placed in a static variable for the remainder of the program. Only exception to that is probably if the Rust program starts the VM itself instead of being invoked as a library.
Export a method called JNI_OnLoad, which will be invoked during System.loadLibray from Java. The instance of the VM is passed as first parameter.
However, the System.loadLibrary call has to be done manually once, as JNA doesn't trigger the JNI_OnLoad here (I guess because it itself is a shared lib that is having this init callback but then invokes the called library through regular C ABI without JNI involved).
Export a custom init function that does the same, must be called explicitly as well, and instead get's a JNI env, which requires another call back through the FFI to get the VM.
Use the JNI_GetCreatedJavaVMs function that can be called in any place, as long as in the context of a JVM. This will give a list of JVM instances but in most cases there should be only one so we can take the first in the list.
I personally am a fan of this one, because there is no extra step required that could be forgotten along the way.
Attach permanently or as daemon
There are two ways of attaching to a thread, as non-daemon or as daemon. Permanently is just a term from the jni crate which means, the thread is kept attached until it shuts down (by using a thread local attach guard that'll call detach_current_thread on drop).
The daemon variant has the only difference that it doesn't prevent the JVM from shutting down. The non-daemon variant will keep the JVM running until all attached threads shut down themselves.
When setting up my tokio runtime I first thought the non-daemon variant is the right choice, and it's mentioned in the docs as well. But as the threads are kept alive as long as the runtime exists, it'll block the JVM from shutting down if the runtime is put into a global static variable.
The workaround is to have a custom unload function that will shut down the tokio runtime. However, this is tricky in Android as there is no global shutdown where it could be invoked, and according to Android docs the process is killed in the end anyway, meaning blocking the JVM from shutting down is likely not relevant in this context.
The daemon variant of course doesn't have this problem, but it's unclear if this might cause some issues in tokio if the JVM shuts down threads without its approval. Well, the whole application is shutting down so it shouldn't matter much. But is there a chance some shutdown step might fail due to the threads being stopped this way?
So long story short, it's probably best to expose both ways of attaching to the JVM, with some good docs, and let the user choose based on their requirements.
The text was updated successfully, but these errors were encountered:
Include some helpers that glue over the percularities of language-specific behavior in uniffi, specifically for the Kotlin/JVM target.
Background
I just recently moved a project at work over from plain JNI to using uniffi for a Rust/Android mixed project. It seemingly worked great at first, but it was only later that I discovered a few footguns here and there.
One of them was the attachment to the current thread for JVM targets, when spawning any kind of threads from the Rust side. In my case a multi-threaded Tokio runtime.
The docs about it were there, but not very prominently and I thought this could be something to be included directly into
uniffi
as utility functions to gloss over percularities of specific targets like JVM, Python, ...Proposal
I propose to include some module into the uniffi crate, that would allow to easily attach the thread to the JVM which is what most people would want for better performance, I'd imagine.
This could be a function that is always present, regardless of the target, and only invoked when in a JVM environment.
Dependencies
I think it would be best to directly implement those APIs instead of relying on the
jni
crate. It brings lots of dependencies with it, and none of them are needed for the JNI APIs in question.After some experimentation I now have a local minimal version that can attach the JVM to the current thread, only would need to copy it over to the uniffi crate.
Open questions
How to detect the target environment
The first thing that comes to mind are feature flags to enable a certain target environment. I'm using the cargo-ndk crate to build for Android and passing in a feature flag like
--features uniffi/kotlin
would probably be the easiest.One danger is of course, when compiling with
--all-features
to compile for any other language it would at some point try to call some JNI functions that wouldn't be present.An alternative could be to provide some extra config but invoking that in Cargo is more tedious.
Maybe there is a better way to do this?
Provide a target independent API or specific
Not sure what would be the best way. Either publicly expose those functions behind a feature flag or have an always present API that does its thing for Kotlin, but is a no-op for other languages. However, the latter feels more convenient.
For example:
How to retrieve the JavaVM instance
Basically there are three choices. In any case, the VM instance is global and can be placed in a static variable for the remainder of the program. Only exception to that is probably if the Rust program starts the VM itself instead of being invoked as a library.
JNI_OnLoad
, which will be invoked duringSystem.loadLibray
from Java. The instance of the VM is passed as first parameter.System.loadLibrary
call has to be done manually once, as JNA doesn't trigger theJNI_OnLoad
here (I guess because it itself is a shared lib that is having this init callback but then invokes the called library through regular C ABI without JNI involved).JNI_GetCreatedJavaVMs
function that can be called in any place, as long as in the context of a JVM. This will give a list of JVM instances but in most cases there should be only one so we can take the first in the list.Attach permanently or as daemon
There are two ways of attaching to a thread, as non-daemon or as daemon. Permanently is just a term from the
jni
crate which means, the thread is kept attached until it shuts down (by using a thread local attach guard that'll calldetach_current_thread
on drop).The daemon variant has the only difference that it doesn't prevent the JVM from shutting down. The non-daemon variant will keep the JVM running until all attached threads shut down themselves.
When setting up my tokio runtime I first thought the non-daemon variant is the right choice, and it's mentioned in the docs as well. But as the threads are kept alive as long as the runtime exists, it'll block the JVM from shutting down if the runtime is put into a global static variable.
The workaround is to have a custom unload function that will shut down the tokio runtime. However, this is tricky in Android as there is no global shutdown where it could be invoked, and according to Android docs the process is killed in the end anyway, meaning blocking the JVM from shutting down is likely not relevant in this context.
The daemon variant of course doesn't have this problem, but it's unclear if this might cause some issues in tokio if the JVM shuts down threads without its approval. Well, the whole application is shutting down so it shouldn't matter much. But is there a chance some shutdown step might fail due to the threads being stopped this way?
So long story short, it's probably best to expose both ways of attaching to the JVM, with some good docs, and let the user choose based on their requirements.
The text was updated successfully, but these errors were encountered: