Interop with Android, iOS and WASM in the same project

Otávio Pace

@otaviopace

@pagarme

Interoperability

FFI

What is FFI?

Foreign Function Interface

Rust

#[no_mangle]
pub extern "C" fn add_two(a: usize) -> usize {
    a + 2
}





#[no_mangle]
pub extern "C" fn foo(...) {
    ...
}






#[no_mangle]
pub extern "C" fn bar(...) -> Z {
    ...
}

Node.js

const ffi = require('ffi');

const rustCoolLib = ffi.Library(
  'rustCoolLib',
  {
    add: [ 'int', [ 'int' ] ]
  }
);

rustCoolLib.add_two(1); // 3




rustCoolLib.foo(...)




const z = rustCoolLib.bar(...)

Why use FFI?

Performance

Python Program

Python Library

C Library

via FFI

Use of existing code/libraries

Concentrate common logic in one library

Android App

IOS App

Web App

.NET App

Rust Library

How to do this in Rust?

Rust tools for FFI

pub extern "C" fn abc() {
    ...
}
extern "C" {
    fn random() -> f64;
}
#[no_mangle]
pub extern "C" fn abc() {
    ...
}
#[repr(C)]
pub enum Weather {
    Hot,
    Cold,
}

Rust library (crate)

*.so, *.a, *.wasm, ...

cargo build --target xxx

Program in another language

comunicates via FFI

cargo build --target
aarch64-linux-android
armv7-linux-androideabi
i686-linux-android
target/
-aarch64
-armv7
-i686
*.so
jniLibs/

Android

link

pub struct MyCoolStruct;

impl MyCoolStruct {
    pub fn new() -> Self { MyCoolStruct }
}
pub struct MyCoolStruct;

impl MyCoolStruct {
    pub fn new() -> Self { MyCoolStruct }
}

use jni::objects::JObject;
use jni::sys::jlong;
use jni::JNIEnv;

#[no_mangle]
pub extern "C" fn Java_com_cool_coolandroidproject_Cool_create(
    _env: JNIEnv,
    _this: JObject
) -> jlong {
    let cool_struct = MyCoolStruct::new();

    let boxed_cool_struct = Box::new(cool_struct);

    Box::into_raw(boxed_cool_struct) as jlong
}
package com.otaviopace.coolandroidproject

class Cool {
    private external fun create(): Long
}
package com.otaviopace.coolandroidproject

class Cool {
    private external fun create(): Long

    // cool struct memory address
    private val coolStruct: Long
}
cargo lipo
target/
-universal

...

*.a

iOS

add as a library
add bridging header
pub struct MyCoolStruct;

impl MyCoolStruct {
    pub fn new() -> Self { MyCoolStruct }
}

#[no_mangle]
pub extern "C" fn create_cool() -> *mut MyCoolStruct {
    let cool_struct = MyCoolStruct::new();

    let boxed_cool_struct = Box::new(cool_struct);

    Box::into_raw(boxed_cool_struct)
}
#ifndef cool_h
#define cool_h

// opaque pointer struct definition
typedef struct MyCoolStruct MyCoolStruct;

MyCoolStruct* create_cool();

#endif
pkg/
*.wasm

WebAssembly

*.js
www/
wasm-pack build
consume as npm package
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct MyCoolStruct;

impl MyCoolStruct {
    pub fn new() -> Self { MyCoolStruct }
}

#[wasm_bindgen]
pub fn create_cool() -> MyCoolStruct {
    MyCoolStruct::new()
}

Ok, how to do those three at the same time?

One more tool

#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct Person {
    name: String,
}
[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3.16"

Structuring the Rust library

An idea

// lib.rs

mod common;
// ... any other common modules

#[cfg(target_arch = "wasm32")]
mod wasm;

#[cfg(target_os = "ios")]
mod ios;

#[cfg(target_os = "android")]
mod android;
// wasm.rs

use crate::common::*;

// public WebAssembly interface
// ios.rs

use crate::common::*;

// public iOS interface
// android.rs

use crate::common::*;

// public Android interface

An example project

The DOOM fire!

0

36

let mut pixels: Vec<u8>;
vec![26, 22, 24, 36, ...];
// lib.rs

// two common modules
mod pixel_board;
mod random;

#[cfg(target_arch = "wasm32")]
mod wasm;

#[cfg(not(target_arch = "wasm32"))]
mod standard_ffi;

#[cfg(target_os = "android")]
#[allow(non_snake_case)]
mod android;

The entrypoint

// pixel_board.rs

use crate::random::*;

#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
pub struct PixelBoard {
    pixels: Vec<u8>,
    width: usize,
    height: usize,
}

impl PixelBoard {
    // all the algorithm's logic
    ...
}

Library's core logic

// wasm.rs

use js_sys::Function;
use wasm_bindgen::prelude::*;

use crate::PixelBoard;

#[wasm_bindgen]
pub fn create_board(fire_width: usize, fire_height: usize) -> PixelBoard {
    PixelBoard::new(fire_width, fire_height)
}

#[wasm_bindgen]
pub fn create_fire_source(board: &mut PixelBoard) {
    board.create_fire_source();
}

#[wasm_bindgen]
pub fn calculate_fire_propagation(board: &mut PixelBoard, render_callback: Function) {
    board.calculate_fire_propagation(convert_render_callback(render_callback));
}

WebAssembly Interface

board.calculate_fire_propagation(Box::new(|pixels_vec| {

    // do my rendering by reading pixels_vec

}));
use js_sys::Function;
use wasm_bindgen::JsValue;

fn convert_render_callback(render: Function) -> Box<Fn(&[u8])> {
    Box::new(move |pixels: &[u8]| {
        render
            .call1(&JsValue::NULL, &to_uint8_array(pixels))
            .expect("Error calling JS `render` function");
    })
}
// standard_ffi.rs

use crate::pixel_board::PixelBoard;

#[no_mangle]
pub extern "C" fn create_board(fire_width: usize, fire_height: usize) -> *mut PixelBoard {
    let pixel_board = PixelBoard::new(fire_width, fire_height);

    let boxed_pixel_board = Box::new(pixel_board);

    Box::into_raw(boxed_pixel_board)
}

#[no_mangle]
pub extern "C" fn create_fire_source(pixel_board: *mut PixelBoard) {
    if pixel_board.is_null() {
        return;
    }

    let pixel_board = unsafe { &mut *pixel_board };
    pixel_board.create_fire_source();
}

#[no_mangle]
pub extern "C" fn free_board(pixel_board: *mut PixelBoard) {
    unsafe {
        Box::from_raw(pixel_board);
    }
}

Standard FFI (iOS)

// android.rs

use jni::objects::JObject;
use jni::sys::{jint, jlong};
use jni::JNIEnv;

use crate::pixel_board::PixelBoard;
use crate::standard_ffi;

#[no_mangle]
pub extern "C" fn Java_com_otaviopace_doomfireandroid_DoomFire_createBoard(
    _env: JNIEnv,
    _this: JObject,
    width: jint,
    height: jint,
) -> jlong {
    standard_ffi::create_board(width as usize, height as usize) as jlong
}

#[no_mangle]
pub extern "C" fn Java_com_otaviopace_doomfireandroid_DoomFire_createFireSource(
    _env: JNIEnv,
    _this: JObject,
    board_ptr: jlong,
) {
    standard_ffi::create_fire_source(board_ptr as *mut PixelBoard);
}

Android Interface (JNI)

Limitations

  • WebAssembly:
  • Android
  • iOS
  • No generics (type parameters)
  • No lifetime parameters
  • All
  • Very few examples online
  • No function pointers
  • Callbacks can only use parameters data

The biggest challenge

Matching types between host and guest language

References

Thanks to:

Marcela Zilliotto

Philipe Paixão

Pagar.me

Deivis Wingert

Mateus Moog

Leonam Dias

Rodrigo Melo

Allan Jorge

Also thanks to:

Kassiano

Murilo da Paixão

Filipe Deschamps

That's it, thank you all! :)

Interop with Android, iOS and WASM in the same project

By otaviopace

Interop with Android, iOS and WASM in the same project

Talk for Rust LATAM 2019

  • 2,228