Cookie Consent by Privacy Policies Generator Vegapit
Creating a JavaFX GUI for Rust WebAssembly using Asmble and Kotlin
Last updated: 2020-01-29T11:27:11

When it comes to creating modern looking Desktop applications, JavaFX is the tried-and-tested weapon of choice. The rise of the Kotlin programming language as a Java replacement makes the proposition even more appealing as it comes with new powerful features while cutting down on verbosity. The good news is that it can be interfaced with a Rust WebAssembly library thanks to the Asmble tool. This tutorial is an example of how this can be done.

Building the WASM library

Our objective is to create the JavaFX version of the Rust/Elm interfacing example covered in a previous article. The tool is a simple financial option pricer that takes a String and several Double as input. The very first step is to compile the WebAssembly library with all the functions we will need. As a String type will be passed as input to our WASM pricer function, an extra layer of complexity comes with the task. As the examples in the Asmble Github repository directory suggest, a String type needs to be handled as a pointer to an Array of u8 for Asmble to process. Here is the lib.rs code we will use (the black76.rs file used is the same as the one used in the Rust/Elm project):

mod black76;

use crate::black76::{Black76, CallPut};
use std::alloc;
use std::alloc::Layout;
use std::mem;
use std::str;

#[no_mangle]
pub extern "C" fn price_black76(str_ptr: *mut u8, len: usize, s: f64, k: f64, t: f64, v: f64, r: f64) -> f64 {
    unsafe {
        let bytes = Vec::<u8>::from_raw_parts(str_ptr, len, len);
        let callput = str::from_utf8_unchecked(&bytes);
        let cp = match callput {
            "Call" => CallPut::Call,
            "Put" => CallPut::Put,
            _ => panic!("Option type unknown")
        };
        let opt = Black76::new(cp, s, k, t, v, r);
        opt.price()
    }
}

#[no_mangle]
pub extern "C" fn delta_black76(str_ptr: *mut u8, len: usize, s: f64, k: f64, t: f64, v: f64, r: f64) -> f64 {
    unsafe {
        let bytes = Vec::<u8>::from_raw_parts(str_ptr, len, len);
        let callput = str::from_utf8_unchecked(&bytes);
        let cp = match callput {
            "Call" => CallPut::Call,
            "Put" => CallPut::Put,
            _ => panic!("Option type unknown")
        };
        let opt = Black76::new(cp, s, k, t, v, r);
        opt.delta()
    }
}

#[no_mangle]
pub extern "C" fn alloc(size: usize) -> *mut u8 {
    unsafe {
        let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
        alloc::alloc(layout)
    }
} // Taken from Asmble Github rust-string example

#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut u8, size: usize) {
    unsafe  {
        let layout = Layout::from_size_align(size, mem::align_of::<u8>()).unwrap();
        alloc::dealloc(ptr, layout);
    }
} // Taken from Asmble Github rust-string example

The str_ptr and len arguments in the pricing functions are an equivalent representation of an input String. A Kotlin class will manage the transformation of a Java String to this format as we will see later. The alloc and dealloc functions will be used to handle the memory management of the created pointers to the String objects. The corresponding Cargo.toml file is pretty succinct as we only define our single dependency and set the crate type to a C-dynamic library:

[package]
name = "rustwasm"
version = "0.1.0"
authors = ["Vegapit <info@vegapit.com>"]
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
statrs = "0.11"

The library needs to be built using wasm32-unknown-unknown as target (no need for wasm-pack for Asmble). We can achieve this with the following command: cargo build --target wasm32-unknown-unknown

Generating the Java wrapper class

The next step is to use the Asmble tool to compile the newly created WASM file into Java Bytecode. The command we need to use (since we built a file called rustwasm.wasm ) is the following:

path/to/asmble compile /path/to/rustwasm.wasm RustWasm -log info

This should create a file called RustWasm.class which can then be imported into a Kotlin project.

Importing and using the Java wrapper class

The easiest way to implement a JavaFX Kotlin project is to use IntelliJ Idea. The community edition comes with the Scene Builder which is a really powerful tool for building GUIs visually. It can be done in other ways so we will just go through the general guidelines on the process to follow.

To import our class file into our project, we just create a wasm folder in the project root, put the RustWasm.class into it and register the directory in your project settings ( File > Project Structure... > Modules > Dependencies in Intellij Idea).

The RustWasm class is now accessible from our Kotlin code, but it needs further wrapping to handle the String input correctly. We then create the following class called WasmLinker:

package javafxtut

import RustWasm
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets

class WasmLinker(max_memory: Int) {

    private val rustwasm = RustWasm(max_memory)

    fun price(callput: String, s: Double, k: Double, t: Double, v: Double, r: Double) : Double? {
        val ptr_callput = ptrFromString(callput)
        return if(ptr_callput != null) {
            rustwasm.price_black76(ptr_callput.offset, ptr_callput.size, s, k, t, v, r)
        } else {
            null
        }
    }

    fun delta(callput: String, s: Double, k: Double, t: Double, v: Double, r: Double) : Double? {
        val ptr_callput = ptrFromString(callput)
        return if(ptr_callput != null) {
            rustwasm.delta_black76(ptr_callput.offset, ptr_callput.size, s, k, t, v, r)
        } else {
            null
        }
    }

    // Get Pointer from String -- Adapted from Asmble Github rust-string example
    private fun ptrFromString(str: String): Ptr? {
        val bytes: ByteArray = str.toByteArray(StandardCharsets.UTF_8)
        val ptr = Ptr(bytes.size)
        ptr.put(bytes)
        return ptr
    }

    // Pointer Manager -- Adapted from Asmble Github rust-string example
    inner class Ptr(val offset: Int, val size: Int) {

        constructor (size: Int) : this(rustwasm.alloc(size), size)

        fun put(bytes: ByteArray?) { // Not thread safe
            val memory: ByteBuffer = rustwasm.memory
            memory.position(offset)
            memory.put(bytes)
        }

        @Throws(Throwable::class)
        protected fun finalize() {
            rustwasm.dealloc(offset, size)
        }

    }
}

The primary constructor of WasmLinker takes an Int as argument, which represents the maximum memory allocated to its usage. The maximum memory allocation picked in project was the same one used in the Asmble rust-simple example:

...
		private val PAGE_SIZE = 65536
		private val MAX_MEMORY = 20 * PAGE_SIZE
		private val wasm = WasmLinker(MAX_MEMORY)
...

The inner class Ptr handles the memory allocation of the String pointer and uses the alloc and dealloc functions defined in the Rust code. Finally, here is the animated screenshot showing the application in action.

We have not discussed the code behind the interface mainly because it is not the point of the post. For those curious to understand this side of the development, I would refer to tutorials which would cover the use of FXML with Kotlin.

If you like this post, follow me on Twitter and get notified on the next posts.