Automatic JavaVM and Context detection using Rust on Android
23 July 2026
When you’re running Rust code in the context of an Android app, if you want to work with Android-specific functionality over JNI you tend to need three things:
- A reference to the running Java virtual machine.
- A reference to a suitable
Context, often theApplicationContext. - Some helper code on the Java side to provide APIs with required callbacks, and so on.
My cross-platform crate netwatcher has to do all of these when it finds itself running on an Android. It handles items 1 and 2 in a very straightforward manner: it exposes a public function where you pass in both of them. It’s up to the developer to do the appropriate plumbing to call through from Java or Kotlin, passing along their Context object. For item 3, I was happy to discover (that is, copy from someone else) a fiddly-but-dependable technique involving injecting DEX bytecode at runtime, which I wrote about previously.
As a crate developer, the holy grail is to get all of these runtime dependencies automatically without the end user having to do anything other than put the crate in Cargo.toml and use the API normally. They will be sad if their code suddenly breaks weirdly when they cross-compile it to Android. The difficulty level is increased by being a mere dependency within a larger Rust binary—if we’re just one piece of the final .so then we can’t/shouldn’t hook into JNI_OnLoad (without coordination through the final library), which is otherwise a reliable way to get the JavaVM* pointer.
The other day I was following some links between issues on GitHub and I noticed that the Matrix Rust SDK has apparently solved items 1 and 2, making them entirely automatic (PR #1, PR #2). I was intrigued and went on a deep dive to see how this worked. What I found is… well it’s pretty out there. For now I’ve decided to leave netwatcher the way it is, even if it’s more annoying for the developer.
The hacks involved are pretty impressive so here I want to dissect how they work and briefly discuss the risks. To be clear, I’m not trying to put anyone on blast for writing or using these techniques. It’s a noble cause and if you get it wrong it’s probably just going to crash.
Let’s begin with item 2 since it’s more straightforward. There is a class in Android called ActivityThread which you won’t find in the documentation. It has a static method currentActivityThread(), and when you get the instance back you can call getApplication(). This returns the Application that you can use as a Context. JNI can give you a reference to the class and help you call the methods. Too easy.
// Get the current activity thread
let activity_thread = env
.call_static_method(
"android/app/ActivityThread",
"currentActivityThread",
"()Landroid/app/ActivityThread;",
&[],
)?
.l()?;
// Then get the application context
let context = env
.call_method(activity_thread, "getApplication", "()Landroid/app/Application;", &[])?
.l()?;
The problem is that ActivityThread and its methods aren’t officially part of the Android SDK and there are policies around using non-SDK interfaces. When I look up currentActivityThread() and getApplication() in their spreadsheet they are both marked unsupported. This means Android turns a blind eye: the call will be permitted, but they’re allowed to break how this works at any time for any reason.
Going by a quick GitHub search this technique gets a fair amount of use but it still gives me pause. I don’t want to suddenly have to rearchitect my library because a new version of Android changes how ActivityThread works, or handle bug reports from people who are running strange OEM Android distributions where up is down and down is up. For now I intend to stick with the supported API.
For item 1 we must turn to the crate jvm-getter, which is where Matrix has ended up. This crate helpfully begins its README with a warning icon and suggests alternatives.
The game here is to locate and call a function called JNI_GetCreatedJavaVMs. This is an official JNI invocation function that gives you a list of JavaVM* pointers. In an Android app you would expect there to be just one VM, so you can pass it a 1-length array and now you have the pointer you need. This function is relatively unusual in JNI because it’s free-standing, an exported symbol you can look up. For most JNI work you would instead start with a JavaVM* or a JNIEnv* and use its function table.
This function has always been exported. What has changed over time is whether it can be discovered by application NDK code. Up to API level 23, you could use dlopen and dlsym to look it up by name and then call it. In API levels 24–30 linker namespaces were introduced and JNI_GetCreatedJavaVMs was no longer accessible, so dlsym fails. Then from API level 31 onward, Android made libnativehelper.so a public NDK library and this JNI function became available to dlsym officially.
This quirk bit Matrix: originally they used libloading (a cross-platform wrapper around the dlopen/dlsym technique) to look up the symbol and call it (PR #1). A bug report came in that it crashes on Android 11 and they switched to using the jvm-getter crate, which offered a workaround when dlsym fails.
So, that workaround. Buckle up, kids. What it does is this:
- Use a system property to learn the name of the Android runtime library, typically
libart.so. - Use
dl_iterate_phdrto walk all the shared objects that have been loaded into the current process. For each one, this invokes a callback. - In the callback, look for the one corresponding to
libart.so. When you see it, record the base address at which it’s been loaded into memory, and the full path on disk to the ELF file. - Read the contents of the ELF file into memory.
- Use the
goblincrate to parse the ELF file, find the symbol table, and locate the symbol whose name matchesJNI_GetCreatedJavaVMs. - Take that symbol’s relative address.
- Add the symbol’s relative address to the base address of
libart.sowhich was recorded earlier. - Cast this to a function pointer and return it to the caller, which then calls it to find the JavaVM.
Now that’s persistence. Notice that the linker namespaces aren’t an isolation boundary. If you can find the address, you can still call it.
While this is plainly nuts, on balance it doesn’t seem too bad. There’s nothing wrong with locating JNI_GetCreatedJavaVMs with dlopen and dlsym—Android explicitly makes it available to find on most modern versions and this will be a reliable method for item 1 going forward. The tricky part is the gap in API levels where the symbol isn’t readily available, if you don’t want to drop support for older devices yet.
The thing about the ELF-parsing workaround is that all the relevant Android versions are in the past. They’re old phones or tablets that aren’t getting updates. libart.so isn’t going to wake up one day with stripped symbols[1]. If this workaround turns out to work well across a range of device types then it’s probably going to keep working.
Unfortunately, solving item 1 alone isn’t enough to simplify my public API. If I’m not prepared to use an unsupported method to get the Context then I need a configuration method anyway, and it’s just as easy to grab the JavaVM* at the same time.
We just need a slightly better way to get an application context from the NDK and everything will be perfect…
- I suspect
jvm-gettercould be made more resilient here by searchingdynsymsinstead ofsyms. Since theJNI_GetCreatedJavaVMsimplementation has to be exported, it must be named and cannot be stripped from this particular symbol table. ↩︎
Serious Computer Business Blog by Thomas Karpiniec
Posts RSS, Atom