A blog of sorts

Betting on the wrong horse

A few years ago I wrote a blog post on my approach with Jextract and the new (at the time unstable) Java Foreign Function and Memory API. That API has since left its preview status in Java 22, and I think it is a significant improvement over the Java Native Interface (JNI) that has existed long before it. Primarily of benefit is the Java code being able to consume a C header that wasn’t written specifically for Java (like most languages doing FFI - if I’m consuming a C library header in LuaJit the writer of the library didn’t need to spend any time on their code explicitly supporting Lua).

However, what I failed to consider is just how long the tooling might take to catch up. I can’t migrate the cross platform Compose UI code I wrote or get anywhere with cross compiling the Rust engine I wrote, because to use those java.lang.foreign APIs to communicate between the two I first need to go to a future where Android supports them and the present day isn’t that.

At work, one thing I learned working in a scrum team is the value in deliberately attempting something you’re not sure is possible before you even commit to doing all the other dependant things you know you can do and can estimate how long you need to do them. Spiking something only to find out that there is some impediment to proceeding is very valuable information if you ensure to do that first. It’s much worse to be days into a task only to find out some extremely important step in that chain isn’t something that is feasible or even possible. Actively seeking out the work most likely to fail requires a mindset change, shying away from failure or even risk of failure is very easy.

Soon into trying to migrate my compose app to support Android I realised I was never going to be able to build the Jextract code. To my credit this was never something I had really considered when I started building the project, I started out with a CLI interface and moved to Compose to get a GUI for desktop. This was very annoying because I could have used UniFFI or any other established tool that relies on JNI or JNA to build the bridge in a way that would have worked on both platforms from the start had I known and factored in Android’s limitations.

However despite betting on the wrong horse in this instance, or perhaps more accurately betting on a horse not yet ready to compete - if Android adds support for the foreign memory APIs in the future I could see the trade-offs swinging in favour of Jextract again, migrating to UniFFI instead was surprisingly easy. I had deliberately isolated the FFI code in both Rust and Kotlin so that its only role was to call into the other Rust and Kotlin code respectively. The Rust side had its own module responsible for FFI glue that didn’t contain any engine logic itself, and the Kotlin code has its own package implementing generic interfaces for the UI code to use which had almost no knowledge of the FFI implementation at all. Someone forking the app wouldn’t have that hard a time replacing the engine with a Kotlin one, at least not until I make the Rust implementation more complex or rely heavily on math libraries. It was relatively simple to write new UniFFI bindings side by side with the existing C header ones for Jextract in the Rust code. Migrating the Kotlin code over was also straightforward - write new implementation of unchanged interface in same file, delete old implementation and rename new one to match, run. For a brief moment in-between commits the interface got to have 2 implementations.

Discovering I had to rework the entire FFI layer to add support for Android initially felt like failure, ‘why hadn’t I discovered this sooner’, ‘why did I write so much code that I have to delete now’. However actually doing the rework barely touched half the codebase. I even deleted far more lines than I needed to add thanks to UniFFI being a bit higher level than Jextract in terms of its bindings lifting and lowering types for me instead of making me do all the work.

I’m now in a better position to get this app running on my phone, and I realised I needed to change my approach before wasting my time fixing the warnings I had accumulated from not editing the old FFI code for a year and getting new lints from updating Rust. Pivot successful?