-
Notifications
You must be signed in to change notification settings - Fork 255
Inclusion of helpers for language-specific tweaks #2410
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
Comments
That's a very elaborate proposal, but it's difficult to comment without more specifics. Ultimately though, it sounds like calling back into Kotlin from Rust created threads are causing you problems, right? @bendk don't we have some comments or something around this in app-services? |
I remember seeing something about this at one point, but I don't remember writing comments about it. I could definitely be forgetting though. @dnaka91 what happens when you try to call a callback from a tokio-spawned thread? Does it just crash? |
Ah, I think I was referring to https://github.com/mozilla/application-services/blob/main/docs/android-faqs.md#what-challenges-exist-when-calling-back-into-kotlin-from-rust - which does describe the problem but doesn't really offer any solutions. |
I already fixed the problem on my side, and what I mean is the performance issue mentioned in this PR. It is just a bit hard to discover and I think it's worth a new sample in the repo to show a setup with Tokio and a multi-thread runtime. What I'm proposing is, to include that A benefit would also be, that this can be implemented with zero dependencies actually, as the docs use |
I have to admit, I don't understand the JVM issues here. However, I think we could have a general solution that looked something like this:
Would this work for you? |
Basically, but I wonder if JNA even gives you the option to attach the thread to the JVM 🤔. Ideally JNA would detach a thread so soon again and simply keep it attached but I guess they have their reason. So the flow would be like:
That sounds like quite a lot of passing forth and back between Rust, JNA and Kotlin. Ideally this would just do a single call all in Rust by directly doing FFI calls into JNI to attach the current thread. As I mentioned, you can get a list of VM instances through JNI, then use the first one to attach the thread. Another thing to consider is to permanently attach or attach as daemon. This is, again, very JVM specific. I extracted my JNI calls into a minimal setup that would fit well into Uniffi. While trying it out, I noticed that in regular non-Android JVMs you could accidentally make the JVM hang. The reason here is, that permanently attaching a thread prevents the JVM from shutting down. Usually this wouldn't be a problem, but if you ever put a thread handle into a The obvious workaround is to attach as daemon, which simply doesn't prevent the JVM to shut down. That sounds good for most cases, as long as the Rust code is loaded as shared lib anyway. Meaning when the JVM shuts down, we are likely happy with shutting everything else down too. But I don't know how tokio behaves if you steal its threads, even in a shutdown step. And maybe there are some corner cases where you want to attach permanently. So long story short this is eventually something that should be exposed to the user for choosing. |
I'm understanding better now. So this means the Rust code needs a dependency on the
Given that users will need to configure the behavior themselves, I don't think we should have a generic "attach_to_thread" function. It seems better to have a JVM-specific one. This could live in the The other reason to do this is that it simplifies the dependencies management for the Mozilla team. We currently vendor in UniFFI the our monorepo, which requires auditing all of it's dependencies and also vendoring them in -- even if they're behind a feature flag. If this was in a separate crate, then we don't need to deal with that. |
Actually I was hoping to implement it directly (well, I already did). The jni crate brings a variety of dependencies and the part of the JNI API we need doesn't require any of those. So it feels simpler to directly do a few very limited unsafe calls. Let me create a draft PR of my local testing setup that I did so far, maybe it gives a better understanding of how much additional code this means. |
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: