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