Injecting Java from native libraries on Android
3 August 2025
When you're writing Rust and you want cross-platform support, Android is uniquely peculiar. Many OS functions that we take for granted when developing on desktop platforms are more complicated to access. It's not always a problem. If your code is purely computation or it's using std to do basic things like open a TCP socket or write to a file then this tends to work. Possibly those calls are gated by extra permissions that you have to declare in the app's manifest, but this is easy to do.
The fun begins when you want to use more advanced capabilities of the phone. Libc doesn't include an API for controlling Bluetooth adapters after all, nor does the NDK provide one. This functionality is only available to apps via Android's Java SDK. Also, some features that we normally expect to be available on Linux like NETLINK_ROUTE sockets are difficult to secure and Android blocks native code from using them. Again you have to use a Java API to access that information, where Android has more over say over how it works and what you can see.
It is possible to access these Java-based things from Rust through the power of JNI. From native code you can use a JNI library to instantiate classes and call methods on Java objects from outside the JVM. Java classes can declare methods that will be implemented by native compiled code, enabling them to invoke native code synchronously.
The fun thing about Android is that sometimes you need to define a class in order to use the API. If you want to get callbacks about certain events then maybe you need to provide an instance of a class that has a certain superclass in order to receive those events.
Until recently I was seriously mistaken about things. I thought that you couldn't create new classes from Rust, only instantiate ones that the JVM already knew about. If you're working under this perspective, either Rust on Android is limited to doing things that don't require callbacks or you have to ensure that the classes you need already exist in the JVM.
If you're an app developer that offloaded some of the logic to Rust then this wouldn't be a big deal. If your Rust needs an extra class or two to do its job then that's no problem, you simply write those classes into your app and they're ready to go.
It's more problematic when you're writing a Rust library intended for consumption by other Rust developers. If somebody adds your library to Cargo.toml and this happens to be in the context of an Android project, how the heck is the app supposed to know that you need extra classes? Why should they have to care about it anyway?
The thing that tripped me is that JNI has a technique for injecting classes and Android's own JNI Tips documentation explicitly says that they don't support it:
- DefineClass is not implemented. Android does not use Java bytecodes or class files, so passing in binary class data doesn't work.
This is technically true but also unhelpful. If you use a DexClassLoader then you can achieve the same thing: compile some Java code in advance and inject it into the JVM at runtime.
Once you're aware of this it's magnificent. If your Rust code wants to support Android but it needs some Java to do its job then it can bring that along in a self-contained manner and inject it and the person who included your library doesn't have to worry about a thing beyond editing Cargo.toml.
There are Rust crates taking advantage of this functionality right now. It's not a secret and I certainly didn’t invent it, but I don't believe this technique is nearly as well-known as it should be and that's why you're reading this post.
Here's slint loading its Java helper via a classes.dex which it built in build.rs. My own crate netwatcher is a bit simpler. It similarly loads a Java helper which it built in build.rs. The general flow is this:
- Have some .java files in your source tree.
- Take advantage of the android-build crate to help compile them into class files.
- Again use the android-build crate to produce a classes.dex.
- Use the include_bytes! macro to embed the content of classes.dex directly in your Rust shared library.
- Use some form of DexClassLoader to inject those classes into the JVM.
- Register the native methods in those class(es), since this won't happen automatically for classes loaded dynamically like this.
I've been writing cross-platform mobile software for a long time and I'm honestly a little embarrassed that I didn't come across this earlier. iOS has always been much simpler—many of Apple's frameworks come with a C interface that's easy to consume from Rust, and if you need to talk to the ObjC runtime then that's entirely feasible. Either way you can do what you need to do without trampolining through app code. It is a great relief that this same level of modularity is available on Android. If you're writing a Rust library that needs to work on Android then please check this out. It will make things much easier for your users.
Serious Computer Business Blog by Thomas Karpiniec
Posts RSS, Atom