Interoperabilidade com Android, iOS e WASM no mesmo projeto

Otávio Pace

@otaviopace

@pagarme

Interoperabilidade

FFI

O que é 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(...)

Por que usar FFI?

Performance

Programa em Python

Biblioteca em Python

Biblioteca em C

via FFI

Utilização de código/bibliotecas já existentes

Concentrar uma lógica em comum em apenas uma biblioteca

Android App

IOS App

Web App

.NET App

Biblioteca em Rust

Como fazer isso em Rust?

Rust tools para 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,
}

Biblioteca em Rust (crate)

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

cargo build --target xxx

Program em outra linguagem

comunica 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.cool.coolandroidproject

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

class Cool {
    private external fun create(): Long

    // endereço de memória da cool struct
    private val coolStruct: Long
}
cargo lipo
target/
-universal

...

*.a

iOS

adicionar como biblioteca
adicionar 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

// definição do ponteiro opaco da struct
typedef struct MyCoolStruct MyCoolStruct;

MyCoolStruct* create_cool();

#endif
pkg/
*.wasm

WebAssembly

*.js
www/
wasm-pack build
consome como um pacote npm
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, como se faz os três ao mesmo tempo?

Mais uma 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"

Estruturando a biblioteca Rust

Uma ideia

// lib.rs

mod common;
// ... e quaisquer outros módulos comuns

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

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

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

use crate::common::*;

// interface pública para WebAssembly
// ios.rs

use crate::common::*;

// interface pública para iOS
// android.rs

use crate::common::*;

// interface pública para Android

Um projeto de exemplo

O fogo do DOOM!

0

36

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

// dois módulos em comum
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;

O ponto de entrada

// 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 {
    // toda a lógica do algoritmo
    ...
}

A lógica principal da biblioteca

// 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));
}

Interface WebAssembly

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

    // fazer renderização lendo o 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)

Limitações

  • WebAssembly:
  • Android
  • iOS
  • Sem generics (type parameters)
  • Sem lifetime parameters
  • Todos
  • Bem poucos exemplos online
  • Sem ponteiros para funções
  • Callbacks só podem usar dados dos parâmetros

O maior desafio

Casar os tipos entre a linguagem hospedeira e a convidada

Referências

Agradeço a:

Marcela Zilliotto

Philipe Paixão

Pagar.me

Deivis Wingert

Mateus Moog

Leonam Dias

Rodrigo Melo

Allan Jorge

E também a:

Kassiano

Murilo da Paixão

Filipe Deschamps

É isso, obrigado! :)

Interoperabilidade com Android, iOS e WASM no mesmo projeto

By otaviopace

Interoperabilidade com Android, iOS e WASM no mesmo projeto

Talk para Rust SP do mês de junho de 2019

  • 895