Rust&C++混成プロジェクトをネイティブとWebAssembly両方へビルドする


作業メモです。説明は気が向いたら。

# 目的

* C++資産を利用しつつRust中心に開発を行う
* ネイティブとブラウザ(WebAssembly)両方で動作できるようにする

# 環境

* rustup
    * wasm32-unknown-emscriptenをtargetに追加しておく
* Emscripten
    * 環境変数を通しておく

# 単一crate構成

## ディレクトリ構成

```
example1
├── Cargo.toml
├── build.rs
└── src
    ├── example.cpp
    └── main.rs
```

## ソースコード

```toml:Cargo.toml
[package]
name = "example1"
version = "0.1.0"

[build-dependencies]
cc = "1.0"
```

```c++:src/example.cpp
#include <iostream>

extern "C" double add_one(double x) {
  std::cout << "hello" << std::endl;
  return x + 1;
}
```

```rust: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));
}
```

```rust:build.rs
extern crate cc;

fn main() {
    cc::Build::new()
        .cpp(true)
        .file("src/example.cpp")
        .compile("example");
}
```

## 実行

### ネイティブ

```shell-session
$ cargo run
hello
hello
4.3
```

### WebAssembly

```shell-session
$ 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

```toml:example2-sub/Cargo.toml
[package]
name = "example2-sub"
version = "0.1.0"

[build-dependencies]
cc = "1.0"
```

```c++:example2-sub/src/example.cpp
#include <iostream>

extern "C" double add_one(double x) {
  std::cout << "hello" << std::endl;
  return x + 1;
}
```

```rust: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))
    }
}
```

```rust:example2-sub/build.rs
extern crate cc;

fn main() {
    cc::Build::new()
        .cpp(true)
        .file("src/example.cpp")
        .compile("example");
}
```

### example2-main

```toml:example2-main/Cargo.toml
[package]
name = "example2-main"
version = "0.1.0"

[dependencies]
example2-sub = { path = "../example2-sub" }
```

```rust:example2-main/src/main.rs
extern crate example2_sub;

use example2_sub::add_two;

fn main(){
    println!("{}", add_two(2.3));
}
```

## 実行

### ネイティブ

```shell-session
$ cargo run -p example2-main
hello
hello
4.3
```

### WebAssembly

```shell-session
$ 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
```

## ソースコード

```toml:Cargo.toml
[workspace]
members = [
  "example3-sys",
  "example3-sub",
  "example3-main"
]
```

```toml:example3-sys/Cargo.toml
[package]
name = "example3-sys"
version = "0.1.0"

[build-dependencies]
cc = "1.0"
bindgen = "0.26.3"
```

### example3-sys

```c++:example3-sys/src/example.cpp
#include <iostream>

extern "C" double add_one(double x) {
  std::cout << "hello" << std::endl;
  return x + 1;
}
```

```c++:example3-sys/src/ecample.h
double add_one(double);
```

```rust: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

```toml:example3-sub/Cargo.toml
[package]
name = "example3-sub"
version = "0.1.0"

[dependencies]
example3-sys = { path = "../example3-sys" }
```

```rust: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

```toml:example3-main/Cargo.toml
[package]
name = "example3-main"
version = "0.1.0"

[dependencies]
example3-sub = { path = "../example3-sub" }
```

```rust:example3-main/src/main.rs
extern crate example3_sub;

use example3_sub::add_two;

fn main(){
    println!("{}", add_two(2.3));
}
```

## 実行

### ネイティブ

```shell-session
$ cargo run -p example3-main
hello
hello
4.3
```

### WebAssembly

```shell-session
$ 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に追加。

```toml:Carto.toml
[workspace]
members = [
  "example3-sys",
  "example3-sub",
  "example3-main",
  "example3-api"
]
```

```toml:example3-api/Cargo.toml
[package]
name = "example3-api"
version = "0.1.0"

[lib]
crate-type = ["staticlib"]

[dependencies]
example3-sub = { path = "../example3-sub" }
```

```rust: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')
})
```

```shell-session
$ 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`に辿り着くまでが大変だった…。