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
- Wasm game of life: https://rustwasm.github.io/book/game-of-life/introduction.html
- C JNI book: http://jnicookbook.owsiak.org
- 10 tips for Rust FFI: https://www.youtube.com/watch?v=kGj-Fxg5txQ
- Overview of Rust FFI: https://www.youtube.com/watch?v=ctffjzdgGvc
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,250