作業メモです。説明は気が向いたら。
目的
- C++資産を利用しつつRust中心に開発を行う
- ネイティブとブラウザ(WebAssembly)両方で動作できるようにする
環境
- rustup
- wasm32-unknown-emscriptenをtargetに追加しておく
- Emscripten
- 環境変数を通しておく
単一crate構成
ディレクトリ構成
example1
├── Cargo.toml
├── build.rs
└── src
├── example.cpp
└── main.rs
ソースコード
Cargo.toml
[package]
name = "example1"
version = "0.1.0"
[build-dependencies]
cc = "1.0"
src/example.cpp
#include <iostream>
extern "C" double add_one(double x) {
std::cout << "hello" << std::endl;
return x + 1;
}
src/main.rs
use std::os::raw::c_double;
extern{
fn add_one(x: c_double) -> c_double;
}
fn add_two(v: f64) -> f64 {
unsafe {
add_one(add_one(v))
}
}
fn main(){
println!("{}", add_two(2.3));
}
build.rs
extern crate cc;
fn main() {
cc::Build::new()
.cpp(true)
.file("src/example.cpp")
.compile("example");
}
実行
ネイティブ
$ cargo run
hello
hello
4.3
WebAssembly
$ cargo build --target=wasm32-unknown-emscripten
$ cd target/wasm32-unknown-emscripten/debug/
$ node example1.js
hello
hello
4.3
複数crate構成
workspaceを使って複数のcrateからプロジェクトを構成します。
ディレクトリ構成
example2
├── Cargo.toml
├── example2-main
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── example2-sub
├── Cargo.toml
├── build.rs
└── src
├── example.cpp
└── lib.rs
ソースコード
Cargo.toml
[workspace]
members = [
"example2-sub",
"example2-main"
]
example2-sub
example2-sub/Cargo.toml
[package]
name = "example2-sub"
version = "0.1.0"
[build-dependencies]
cc = "1.0"
example2-sub/src/example.cpp
#include <iostream>
extern "C" double add_one(double x) {
std::cout << "hello" << std::endl;
return x + 1;
}
example2-sub/src/lib.rs
use std::os::raw::c_double;
extern {
fn add_one(x: c_double) -> c_double;
}
pub fn add_two(v: f64) -> f64 {
unsafe {
add_one(add_one(v))
}
}
example2-sub/build.rs
extern crate cc;
fn main() {
cc::Build::new()
.cpp(true)
.file("src/example.cpp")
.compile("example");
}
example2-main
example2-main/Cargo.toml
[package]
name = "example2-main"
version = "0.1.0"
[dependencies]
example2-sub = { path = "../example2-sub" }
example2-main/src/main.rs
extern crate example2_sub;
use example2_sub::add_two;
fn main(){
println!("{}", add_two(2.3));
}
実行
ネイティブ
$ cargo run -p example2-main
hello
hello
4.3
WebAssembly
$ EMCC_FORCE_STDLIBS=1 cargo build -p example2-main --target=wasm32-unknown-emscripten
$ cd target/wasm32-unknown-emscripten/debug/
$ node example2-main.js
hello
hello
4.3
crateを跨いだ時に、emccが標準ライブラリをリンクしてくれなかったので、EMCC_FORCE_STDLIBS=1
を使用している。
複数crate構成 + bindgen
C++のラッパー層をbindgenで生成する。
ディレクトリ構成
example3
├── Cargo.toml
├── example3-main
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── example3-sub
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── example3-sys
├── Cargo.toml
├── build.rs
└── src
├── example.cpp
├── example.h
└── lib.rs
ソースコード
Cargo.toml
[workspace]
members = [
"example3-sys",
"example3-sub",
"example3-main"
]
example3-sys/Cargo.toml
[package]
name = "example3-sys"
version = "0.1.0"
[build-dependencies]
cc = "1.0"
bindgen = "0.26.3"
example3-sys
example3-sys/src/example.cpp
#include <iostream>
extern "C" double add_one(double x) {
std::cout << "hello" << std::endl;
return x + 1;
}
example3-sys/src/ecample.h
double add_one(double);
example3-sys/build.rs
extern crate cc;
extern crate bindgen;
use std::env;
use std::path::PathBuf;
fn main() {
cc::Build::new()
.cpp(true)
.file("src/example.cpp")
.compile("example");
let bindings = bindgen::Builder::default()
.header("src/example.h")
.trust_clang_mangling(false)
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings");
}
Emscriptenが生成するシンボル名と、bindgenが生成するlink_name
が一致しないので、trust_clang_mangling(false)
でbindgenがlink_name
を生成しないようにする。
example3-sub
example3-sub/Cargo.toml
[package]
name = "example3-sub"
version = "0.1.0"
[dependencies]
example3-sys = { path = "../example3-sys" }
example3-sub/src/lib.rs
extern crate example3_sys;
use example3_sys::add_one;
pub fn add_two(v: f64) -> f64 {
unsafe {
add_one(add_one(v))
}
}
example3-main
example3-main/Cargo.toml
[package]
name = "example3-main"
version = "0.1.0"
[dependencies]
example3-sub = { path = "../example3-sub" }
example3-main/src/main.rs
extern crate example3_sub;
use example3_sub::add_two;
fn main(){
println!("{}", add_two(2.3));
}
実行
ネイティブ
$ cargo run -p example3-main
hello
hello
4.3
WebAssembly
$ EMCC_FORCE_STDLIBS=1 cargo build -p example3-main --target=wasm32-unknown-emscripten
$ cd target/wasm32-unknown-emscripten/debug/
$ node example3-main.js
hello
hello
4.3
JavaScriptへのAPI提供
JavaScriptから呼び出し可能なAPIを提供する。
crate-type
をstaticlib
として、.a
ファイルを生成する。
Emscriptenで.a
ファイルから.js
と.wasm
を生成する。
.a
ファイル自体はCを通じて読み出せるのでPythonやRなどの他言語へのAPI提供もおそらく可能(未検証)。
ソースコード
修正箇所が少ないのでexample3に追加。
Carto.toml
[workspace]
members = [
"example3-sys",
"example3-sub",
"example3-main",
"example3-api"
]
example3-api/Cargo.toml
[package]
name = "example3-api"
version = "0.1.0"
[lib]
crate-type = ["staticlib"]
[dependencies]
example3-sub = { path = "../example3-sub" }
example3-api/src/lib.rs
extern crate example3_sub;
use example3_sub::add_two;
#[no_mangle]
pub fn example() {
println!("{}", add_two(2.3));
}
実行
index.js
const Example = require('./example')
Example().then((module) => {
module.ccall('example')
})
$ cargo build -p example3-api --target=wasm32-unknown-emscripten
$ emcc target/wasm32-unknown-emscripten/debug/libexample3_api.a -o example.js -s EXPORTED_FUNCTIONS='["_example"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall"]' -s MODULARIZE=1
$ node index.js
hello
hello
4.3
感想
EMCC_FORCE_STDLIBS=1
とtrust-clang_mangling
に辿り着くまでが大変だった…。