Rust で libtiff の tiff2pdf を WebAssembly 化する

作ってみました。下記ページ開くと TIFF 画像が WebAssembly で PDF に変換されてダウンロードされます。

https://zxrs.github.io/tiff2pdf_wasm/

# 動機

わが社ではいにしえの図面は TIFF 画像でサーバに置かれています。一方で最近の図面は PDF なので、違うビューワが立ち上がってしまうので作業効率が悪いです。ので、TIFF 画像を自動的に PDF にへんかんする CLI ツールを作りました。このツールは Rust で書かれていて、FFI で libtiff の API を叩いています。WebAssembly に興味があったので 「wasm-pack を使えば、この CLI ツールも簡単に WebAssembly 化してブラウザ上で実行できるんだろう」と試してみたら、いくつか詰まったところがあったので記事にしておきます。

# WebAssembly 化する方法

2023年現在 Rust を WebAssembly にするには以下の方法があるようです。

- wasm-pack/wasm32-unknown-unknown
- cargo-wasi/wasm32-wasi
- emsdk/wasm32-unknown-emscripten

## wasm-pack/wasm32-unknown-unknown

2023年現在 wasm-pack では C への FFI を含む Rust を WebAssembly 化するのは不可能のようです。たぶんこれからも無理そうです。わたしも下記の記事同様 env の罠にかかって wasm-pack での WebAssembly 化はあきらめました。

https://zenn.dev/newgyu/articles/4240df5d2a7d55

## cargo-wasi/wasm32-wasi

下記の記事を参考に llvm-ar と WASI で WebAssembly 化できます。node 上で wasm 化した tiff2pdf は実行できました。~~これを webpack しようとすると失敗するので、これを解決するのが最近のわたしの課題です。~~（追記：webpack できたので後述します。）

https://zenn.dev/yskszk63/articles/implementof-rust-c-wasm

## emsdk/wasm32-unknown-emscripten

emsdk を使用することでブラウザ上で tiff2pdf できる WebAssembly が生成できました。下記の記事を参考にしました。

https://future-architect.github.io/articles/20230605a/

リポジトリはこちら。JPEG とか ZIP のサポートは省いているので変換できない TIFF 画像はいっぱいあると思います。

https://github.com/zxrs/t2p

# libtiff

## Read and write on memory

libtiff の tiff2pdf は fopen とか fclose を使ってファイルの読み書きをしていました。WebAssembly 化するにあたってメモリ上で IO する必要があると思ったので、ここを Rust で書きました。（emscripten にはメモリファイルシステムが用意されているとも知らずに…）

この stackoverflow の回答を参考にしています。

https://stackoverflow.com/questions/4624144/c-libtiff-read-and-save-file-from-and-to-memory

Read はこう。

```rust:t2p/libtiff-sys/src/lib.rs
struct Input<'a>(Cursor<&'a [u8]>);

// tiff 変数に &[u8] の TIFF 画像データが入っている
let input_memory = Input(Cursor::new(tiff));
// 名前は適当で良い
let name = CString::new("MemoryInput")?;
// Read only モードで開く
let mode = CString::new("rm")?;

let input_tiff = unsafe {
    TIFFClientOpen(
        name.as_ptr(),
        mode.as_ptr(),
        &input_memory as *const _ as _, // コールバック関数に渡すデータ
        Some(input_read),
        Some(input_write),
        Some(input_seek),
        Some(dummy_close), // close, size, map, unmap は使用しないので空の関数で良い
        Some(dummy_size),
        Some(dummy_map),
        Some(dummy_unmap),
    )
};

unsafe extern "C" fn input_read(handle: thandle_t, buf: tdata_t, size: tmsize_t) -> tmsize_t {
    // TIFFClientOpen の第 3 引数に渡したデータ
    let input_memory = &mut *(handle as *mut Input);
    // libtiff が用意しているバッファを slice に変換
    let buf = slice::from_raw_parts_mut(buf as *mut u8, size as _);
    // その slice に TIFF データを読み込む
    input_memory.0.read_exact(buf).expect("failed to read.");
    // 読み込んだバイト数を返す
    size
}

unsafe extern "C" fn input_write(_: thandle_t, _: tdata_t, _: tmsize_t) -> tmsize_t {
    0
}

unsafe extern "C" fn input_seek(handle: thandle_t, offset: u64, whence: i32) -> u64 {
    let input_memory = &mut *(handle as *mut Input);
    let pos = match whence {
        // TIFF データの先頭からのオフセット
        0 => SeekFrom::Start(offset as _),
        // TIFF データの Cursor の現在位置からのオフセット
        1 => SeekFrom::Current(offset as _),
        // TIFF データの末尾からのオフセット
        2 => SeekFrom::End(offset as _),
        _ => unimplemented!(),
    };
    // Cursor 位置を動かして、現在の位置を返す
    input_memory.0.seek(pos).expect("failed to seek")
}
```

Rust には Vec、Cursor などがあるので C より簡単に書けますね。

続いて Write。

```rust:t2p/libtiff-sys/src/lib.rs
struct Output<'a>(Cursor<&'a mut Vec<u8>>);

// この Vec に PDF データが書き込まれる
let mut buf = vec![];
let mut output_memory = Output(Cursor::new(&mut buf));

let name = CString::new("MemoryOutput")?;
// Write モードで開く
let mode = CString::new("w")?;

let output_tiff = unsafe {
    TIFFClientOpen(
        name.as_ptr(),
        mode.as_ptr(),
        &output_memory as *const _ as _,
        Some(output_read),
        Some(output_write),
        Some(output_seek),
        Some(dummy_close),
        Some(dummy_size),
        Some(dummy_map),
        Some(dummy_unmap),
    )
};

unsafe extern "C" fn output_read(_: thandle_t, _: tdata_t, _: tmsize_t) -> tmsize_t {
    0
}

unsafe extern "C" fn output_write(handle: thandle_t, data: tdata_t, size: tmsize_t) -> tmsize_t {
    let output_memory = &mut *(handle as *mut Output);
    // libtiff が生成した PDF データを slice にする
    let data = slice::from_raw_parts(data as *const u8, size as _);
    // そのデータを上で用意した Vec に書き込む
    output_memory.0.write_all(data).expect("failed to write.");
    size
}

unsafe extern "C" fn output_seek(_: thandle_t, offset: u64, _: i32) -> u64 {
    offset
}
```

これでメモリ上で読み書きできるようになりました。

## t2p_write_pdf

上記で準備した`input_tiff`と`output_tiff`を`t2p_write_pdf`関数に渡すだけです。第 1 引数は`t2p_init`で取得した T2P 構造体へのポインタです。

```rust:t2p/libtiff-sys/src/lib.rs
let t2p = unsafe { t2p_init() };
let ret = unsafe { t2p_write_pdf(t2p, input_tiff, output_tiff) };
```

しかし、これではうまく行きません。TIFFClientOpen は書き込みモードで開くと最初の 4 バイトに TIFF ヘッダを書き込んで、Cursor のオフセットを 4 進めてしまうのです。ので、`t2p_write_pdf`を呼ぶ前に Cursor を 0 に戻す必要があります。

```rust:t2p/libtiff-sys/src/lib.rs
let output_tiff = unsafe {
    TIFFClientOpen(
        // 省略
    )
};

// Cursor 位置を 0 に戻す
output_memory.0.set_position(0);

let ret = unsafe { t2p_write_pdf(t2p, input_tiff, output_tiff) };
```

エラー処理、メモリの自動開放も追加して libtiff-sys の lib.rs はこうなりました。

https://github.com/zxrs/t2p/blob/main/libtiff-sys/src/lib.rs

# Emscripten

上記の libtiff-sys の`generate_pdf`は引数に slice を取って、戻り値は anyhow::Result に包まれた Vec<u8> となっていますが、これを Emscripten で扱うのは面倒なので C の関数っぽくラップします。MDN に WebP エンコーダを WebAssembly にする例があったので参考にしました。

https://developer.mozilla.org/ja/docs/WebAssembly/existing_C_to_Wasm

```rust:t2p/src/lib.rs
// 生成した PDF データを保存しておくグローバル変数
static BUF: Global<ManuallyDrop<Vec<u8>>> = Global(RefCell::new(None));
struct Global<T>(RefCell<Option<T>>);
unsafe impl<T> Sync for Global<T> {}

// no_mangle 属性でマングリングされないようにする
// 第 1 引数は TIFF データへのポインタ
// 第 2 引数は TIFF データの長さ
// 返り値は PDF データへのポインタ
#[no_mangle]
pub fn generate_pdf(ptr: *const u8, len: usize) -> *const u8 {
    // TIFF データを slice にする
    let tiff = unsafe { slice::from_raw_parts(ptr, len) };
    // PDF に変換
    let pdf = libtiff_sys::generate_pdf(tiff).expect("failed to generate pdf.");
    // 関数抜けたときに PDF データが開放されないように ManuallyDrop に包む（必要ないかも。明示的だからいいか）
    let pdf = ManuallyDrop::new(pdf);
    let ptr = pdf.as_ptr();
    // グローバル変数に PDF データを保存
    *BUF.0.borrow_mut() = Some(pdf);
    ptr
}
```

そして、`EMCC_CFLAGS`に`-s EXPORTED_FUNCTIONS=['_generate_pdf']`を追加して cargo でビルドしてやると JavaScript から`generate_pdf`関数を呼べるようになります。関数名の前に`_`を追加するルールのようです。

```sh:t2p/build.sh
export EMCC_CFLAGS="-O3 -o t2p.js -s EXPORTED_FUNCTIONS=['_generate_pdf','_buf_len','_free_buf','_malloc','_free'] -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_RUNTIME_METHODS=ccall"
cargo build --release --target wasm32-unknown-emscripten
```

続いて JavaScript 側の実装です。`ccall`関数を使って WebAssembly の関数を呼びます。第 1 引数が関数名。第 2 引数が返り値の型。第 3 引数が引数の型。第 4 引数が関数に渡す引数となっています。

```javascript:t2p/index.html
const result_ptr = ccall("generate_pdf", "number", ["number", "number"], [ptr, array.length]);
```

`ccall`関数で使える型としては`string`、`number`、`array`があるようです。当初`array`を使って TIFF データを渡そうとしましたが、これはスタックオーバーフローしてうまくいきませんでした。ので、大きなデータを WebAssembly 側に渡す場合にはヒープを使う必要があります。

```javascript:t2p/index.html
// TIFF データを取得
const response = await fetch("test.tif");
const data = await response.arrayBuffer();
const array = new Uint8Array(data);
// WebAssembly 側にヒープ領域を確保
const ptr = Module._malloc(array.length);
// TIFF データを WebAssembly 側のヒープにコピー
Module.HEAPU8.set(array, ptr);
```

これで TIFF データを WebAssembly に渡して PDF に変換できました。次に WebAssembly 側のヒープにある PDF データを JavaScript 側にもってくる必要があります。`generate_pdf`関数は PDF データへのポインタを返すので、あとは PDF データの長さがあればどうにかなりそうです。ので、PDF データの長さを返す関数を Rust で書きます。

```rust:t2p/src/lib.rs
#[no_mangle]
pub fn buf_len() -> usize {
    BUF.0.borrow().as_ref().expect("no buffer.").len()
}
```

`buf_len`関数を JavaScript 側から呼び出すと PDF データのサイズを取得できます。前述の PDF データへのポインタとサイズでもって、WebAsssembly 側のヒープへの view を作り、データを JavaScript 側にコピーします。

```javascript:t2p/index.html
// PDF データのサイズ取得
const len = ccall("buf_len", "number", [], []);
// WebAssembly 側のヒープへの view を作る
const view = new Uint8Array(Module.HEAPU8.buffer, result_ptr, len);
// JavaScript 側にデータをコピー
const result = new Uint8Array(view);
```

ついに、JavaScript 側に PDF データを持ってこれました！
あとは WebAssembly 側のヒープデータの後始末とか PDF のダウンロード処理を追加して index.html はこうなりました。

https://github.com/zxrs/t2p/blob/main/index.html

# WASI

こちらが WASI 版のもの。

https://zxrs.github.io/tiff2pdf_wasi/

リポジトリはこちら。

https://github.com/zxrs/t2p_wasi

## @wasmer/wasi

WASI を Node やブラウザ上で実行するためのライブラリです。インメモリのファイルシステムがあるので TIFF 画像や、成果物の PDF はこのファイルシステムを使って Rust 側とやり取りしました。

https://github.com/wasmerio/wasmer-js

```javascript:t2p_wasi/index.mjs
// インメモリファイルを開いて
let file = wasi.fs.open("/input", {read: true, write: true, create: true});
// TIFF 画像を書き込む
file.write(tiff);
file.seek(0);

// Rust の main 関数が呼ばれる
wasi.start();

// Rust 側では "/output" に PDF データを書き込むので JavaScript 側に読み込む
const out = wasi.fs.open("/output", {read: true, write: false, create: false});
const pdf = out.read();

// PDF データの後始末
wasi.fs.removeFile("/output");
```

```rust:t2p_wasi/src/main.rs
fn main() -> Result<()> {
    // TIFF データの読み込み
    let tiff = fs::read("/input")?;
    // PDF へ変換
    let pdf = generate_pdf(&tiff)?;
    // インメモリファイルシステムに書き込む
    fs::write("/output", &pdf)?;
    Ok(())
}
```

## webpack

Node で動くようになったので webpack でブラウザ用にまとめます。webpack.config.js はこうなりました。

```javascript:t2p_wasi/webpack.config.js
const webpack = require("webpack");

module.exports = {
    entry: ["./index.mjs"],
    // Buffer がないって言われるので追加（あんまり意味わかってないので後で調べる）
    plugins: [
        new webpack.ProvidePlugin({
            Buffer: ["buffer", "Buffer"]
        })
    ],
    resolve: {
        fallback: {
            "buffer": require.resolve("buffer")
        }
    },
    // wasmer_wasi_js_bg.wasm がないって言われるので無視する 
    externals: {
        "wasmer_wasi_js_bg.wasm": true
    },
    // index.mjs の generate_pdf を index.html から window.generate_pdf() で呼べるようにする 
    output: {
        library: "generate_pdf",
        libraryTarget: "window",
        libraryExport: "default",
    }
};
```

# まとめ

stdio.h とかを使っている C モジュールを FFI で使う Rust を WebAssembly 化するのはつらい。けど、どうにかなった:blush: