Writing Multi-Platform GUI App in Rust
A skippable introduction
There are many ways to make software work on a platform that the developer did not intend. There’s emulation, of course, but that’s bulky. Wine has been a valiant effort to bring games and applications from Windows to Linux, but applications running this way may look worse when transplanted to a new environment. WSL is also an interesting development, but it didn’t receive official GUI support until recently, and there are similar problems with non-native looks.
What if, instead, our application could, using a single code base, run on many different platforms. I know what you’re thinking — I don’t want to write in Java either.
Setting things up
We target the following platforms: Desktop (Win/Linux), Web as well as Mobile (Android). Dioxus, the framework we will be using, also supports macOS and iOS, if you are interested in that.
First, we need to install xbuild, which takes care of some of the annoying aspects of targeting multiple platforms.
cargo install xbuild
Next, we will generate a project template.
x new $project_name
In general, I found the whole rust-mobile organization helpful when working on Android compatibility.
Take a look at these files
Let’s go through the files from the fresh template to find out how it works.
Why do you see a Kotlin file in our beautiful Rust application? We still need some glue to marry the dominant Java platform with our compiled code, and this serves as just that. Actually, TauriActivity
from WRY, a cross-platform WebView rendering library, does the heavy lifting for us.
This is the template configuration for xbuild itself, you can review the code for details, but the defaults work for us.
A word on dependencies
Default dependencies are strongly platform-based, and unfortunately you will often have to work around some crates not supporting all the platforms you need.
Desktop is of course the easiest, most crates support most popular configurations, even if by accident. The problems start with web — wasm runs in a very closed environment (which is good), but it means that your favorite crate died before setting up the login because it tried to access the system timer, and you spent half a day wondering why.
Android can be another problem, again because of the NDK barrier that makes many seemingly obvious things non-trivial. As always, there are ways around this if the community wants them (e.g., Google Play Billing crate, which is part of crossbow, another multiplatform toolkit, this time designed to write Android/iOS games purely in Rust), but expect to have to write glue code in Kotlin once you’re off the beaten path.
Finally, the code
First, let’s take a look at the android glue code.
Above is another Android-specific element. start_app
with no_mangle
makes sure that the resulting library has the start_app
symbol. This tandem of functions also ensures that it doesn’t expand outside the Rust code in case of panic when main
is called.
Speaking of which, this is the main
, or where our mobile and desktop code paths meet. dioxus_desktop::launch(app)
is the entry point to the dioxus application and handles all the platform details behind the scenes.
The WebAssembly version also sets up logging, and instead calls the dioxus_web
crate.
Finally, all platforms meet in the app
function.
RSX
Dioxus uses its own meta-language to describe the UI. It is inspired by React and provides a way to get data into and out of the GUI. The Dioxus guide is a great place to learn about the syntax and capabilities of the language in a vacuum, while the example projects should give you an idea of how things look in less trivial cases.
Adding WebAssembly Support
The xbuild has left us in a state where we are almost ready to run our application as a web application, so let’s finish it off by adding an index.html
file.
The important part here is the div
with id=main
. This is where trunk
will place our application. Let’s install it first.
cargo install trunk
…and then use it to support our application.
trunk serve
You can add --address 0.0.0.0
if you’re in WSL, by default it listens on localhost. Either way, you should be able to see the app when connecting to port 8080
.
Running the app
Other platforms should be supported out of the box. First, let’s see what “devices” x recognizes.
florek@desolace:/mnt/c/dev/try$ x devices
host Linux linux x64 Debian GNU/Linux 5.15.79.1-microsoft-standard-WSL2
adb:emulator-5554 emu64x android x64 Android 13 (API 33)
By default, x run
starts on the first device, which is usually the host. To run it on an emulator, as in the above case, run.
x run --device adb::emulator-5554
Troubleshooting
As you can imagine, between android, web and everything else, there are a lot of moving parts. xbuild has a built-in diagnostic tool under the x doctor
subcommand, where you can see all the tools that xbuild
has detected, which should help with obvious deficiencies. For platform-specific problems, don’t shy away from resources, even if they don’t seem directly related to Rust. I, for example, found out that my debian installation had too old a version of gradle to build the app package, and all I had to do was download the latest version from their site.
Words by Mikołaj Florkiewicz, Senior Engineer
Editing by Kinga Kuśnierz, Content Writer