Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inclusion of helpers for language-specific tweaks #2410

Open
dnaka91 opened this issue Jan 19, 2025 · 0 comments
Open

Inclusion of helpers for language-specific tweaks #2410

dnaka91 opened this issue Jan 19, 2025 · 0 comments

Comments

@dnaka91
Copy link

dnaka91 commented Jan 19, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant