Rustで動的ロードによるプラグインシステムを作る

# はじめに

この記事は[Rustその3 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/rust3) 23日目です。（空いていたので飛び入り参加で）

Rustでプラグインシステムを実装する必要があったので、その時の知見をまとめてサンプルプロジェクトを作ってみました。

ここで言うプラグインシステムとは、例えばGIMPのプラグインのように、バイナリ形式のプラグインを実行時に読み込んで機能追加するものを指します。最近この手の拡張はスクリプト言語でやることが多く、この形式はあまり見かけなくなりました。しかしRustで実装したいならバイナリの動的ロードをするしかないだろう、ということでやってみました。

# 注意

以下のコードは手元の環境のRust1.40.0では動作していますが、Rustコンパイラが保証していない仮定をいくつか置いているので、動く保証はありません。不具合が致命的になるような環境では使わない方がいいと思います。
~~詳細はコメント欄をご覧ください。~~
コメントは消えてしまったので安全性に関しては以下の記事もご覧ください。

https://ubnt-intrepid.netlify.com/plugin-system-with-ffi-safety/

# サンプルプロジェクト

リポジトリはこちらになります。

https://github.com/dalance/rust-plugin-sample

ツール本体とプラグイン二つを含んだワークスペースになっていて、calcが本体でplugin_add/plugin_mulがプラグインです。
電卓プログラムで演算子をプラグインで追加できる、という感じになっています。

実際にプラグインを使っている様子はcalc/src/main.rsにあります。

```rust:calc/src/main.rs
fn main() -> Result<()> {
    let mut pm = PluginManager::new();
    pm.load(&PathBuf::from("./target/debug/libplugin_add.so"))?;
    pm.load(&PathBuf::from("./target/debug/libplugin_mul.so"))?;

    for plugin in pm.plugins() {
        println!("Plugin: {}", plugin.name());
        println!("Calc: 1 {} 2 = {}", plugin.operator(), plugin.calc(1, 2));
    }

    Ok(())
}
```

`pm.load`でプラグインのバイナリをロードして、各プラグインの`calc`で計算しています。
実行すると

```console
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/calc`
Plugin: Add
Calc: 1 + 2 = 3
Plugin: Mul
Calc: 1 * 2 = 2

```

となります。

# 解説

ここから各実装の解説をしていきます。

## `Plugin`トレイト

まず各プラグインが提供する機能をトレイトとして定義します。

```rust:calc/src/lib.rs
pub trait Plugin {
    fn name(&self) -> String;
    fn operator(&self) -> String;
    fn calc(&self, a: u32, b: u32) -> u32;
}
```

電卓として必要なのは`calc`だけですが、表示用に`String`を返すメソッドも付けてみました。

## `PluginAdd`/`PluginMul`

定義したトレイトを実装した`PluginAdd`/`PluginMul`を定義します。

```rust:plugin_add/src/lib.rs
pub struct PluginAdd;

impl Plugin for PluginAdd {
    fn name(&self) -> String {
        String::from("Add")
    }

    fn operator(&self) -> String {
        String::from("+")
    }

    fn calc(&self, a: u32, b: u32) -> u32 {
        a + b
    }
}
```

ここまでは特に動的ロードに関する部分はない普通のRustプログラムです。

## crate-type

普通のRustライブラリはコンパイルすると`.rlib`という拡張子のファイルが生成されますが、これは静的リンク用のライブラリであり、動的ロードはできません。
そこでCargo.tomlにcrate-typeを書いて生成されるバイナリの形式を変更する必要があります。

```toml:Cargo.toml
[lib]
crate-type = ["cdylib"]
```

ここで選択肢が2つあります（たぶん）。簡単に比較してみると

|crate-type|意味|最小サイズ|最大サイズ|
|:--|:--|:--|:--|:--|
|dylib|Rustの動的ロードライブラリ|小さい|大きい|
|cdylib|Cの動的ロードライブラリ|大きい|小さい|

という感じのようです。
RustからRustを呼び出すのでインターフェースがRustでもCでもそれほど違いはないのですが、（現時点のRustコンパイラの実装では）サイズに関する特性が大きく違います。
サンプルでは`PluginAdd`をcdylibに`PluginMul`をdylibにしてみたところ、生成されたバイナリは以下のようになりました。

```console
$ ls -sh target/debug/*.so
2.7M target/debug/libplugin_add.so  624K target/debug/libplugin_mul.so
```

ほとんど同じ実装なのにサイズが4倍くらい違います。これはcdylibはRustの標準ライブラリを含み、dylibは（ロードされる環境がRustなので標準ライブラリの存在は仮定して）標準ライブラリを含まないからです。
これがcdylibの最小サイズが大きい理由で、どれほど小さいプラグインでも必ず3MBくらいかかってしまいます。
（ここからさらに削る方法はありますがそれは置いておいて）

一方「dylibの最大サイズが大きい」というのは現在のRustコンパイラ実装ではdylibは依存ライブラリの全シンボルを（使っていなくても）含んでしまうためです。今回のサンプルでは依存ライブラリがほとんどないので問題になりませんが、実際のプロジェクトではプラグインのサイズが本体より大きくなったりします。

このあたりは仕様が決まっているわけでもなさそうなのでいまいち判断し難いですが、実プロジェクトの方ではプラグインが大きくなりすぎるのを避けるためにcdylibを選択しました。

## `PluginManager`

次に本体側でプラグインをロードするために`PluginManager`を実装します。
ライブラリの動的ロードを行うクレートとしてlibloadingを使います。

```Cargo.toml
[dependencies]
libloading = "*"
```

まず、`PluginManager`の定義です。

```rust:calc/src/lib.rs
pub struct PluginManager {
    plugins: Vec<Box<dyn Plugin>>,
    libraries: Vec<Library>,
}
```

ロードした`Plugin`を保持する`Vec`と、ロードしたライブラリそのものを保持する`Vec`を持ちます。
実装は次のようになります。

```rust:calc/src/lib.rs
impl PluginManager {
    pub fn new() -> Self {
        PluginManager {
            plugins: Vec::new(),
            libraries: Vec::new(),
        }
    }

    pub fn load(&mut self, path: &Path) -> Result<()> {
        let lib = Library::new(path)?;

        let load_plugin: Symbol<extern "C" fn() -> Box<dyn Plugin>> =
            unsafe { lib.get(b"load_plugin") }?;
        let plugin = load_plugin();

        self.plugins.push(plugin);
        self.libraries.push(lib);
        Ok(())
    }

    pub fn plugins(&self) -> &[Box<dyn Plugin>] {
        &self.plugins
    }
}
```

`lib.get(b"load_plugin")`でプラグインから`load_plugin`関数を持ってきて、次の行でその関数を呼んで`Plugin`のポインタを得ます。`Plugin`を得たら`Library`は不要になるように見えますが、`Plugin`のバイナリの実体は`Library`が保持しているので、これがDropされると`Plugin`の関数呼び出しに失敗します。そのため`PluginManager`に`Library`を保持しておく必要があるわけです。

```rust:calc/src/lib.rs
impl Drop for PluginManager {
    fn drop(&mut self) {
        // Plugin drop must be called before Library drop.
        self.plugins.clear();
        self.libraries.clear();
    }
}
```

最後に`PluginManager`にDropを実装します。これは`PluginManager`のメンバのDrop順序を制御するためで、もし`libraries`が先にDropされてしまうと、`plugins`をDropするときにはすでにDrop用のバイナリは解放されてしまっているのでDropに失敗します。（手元の環境ではSEGVしました）

## `load_plugin`

最後に各プラグインに`PluginManager`が呼び出す`load_plugin`関数を実装します。

```rust:plugin_add/src/lib.rs
#[no_mangle]
pub extern "C" fn load_plugin() -> Box<dyn Plugin> {
    Box::new(PluginAdd)
}
```

Pluginをnewしてポインタで返すだけですが、cdylibで使う場合は`#[no_mangle]`が必要です。

## アロケータの指定

コメント欄でご指摘いただいたのですが、cdylib/staticlib以外のtypeでデフォルトのアロケータが決まっていないので明示した方が良いそうです。今回の場合本体とdylibになっている`PluginMul`が該当します。

```rust:calc/src/main.rs
use std::alloc::System;

#[global_allocator]
static ALLOCATOR: System = System;
```

# Rustコンパイラ間の相互運用性について

Twitterでのコメントを見ていて気付きましたが、重要な前提を書き忘れていました…。
このシステムは本体とプラグインで同じバージョンのRustコンパイラを使う前提です。

本体とプラグインで同じメモリレイアウトやメモリアロケータが採用されていることを想定している箇所がいくつかあり（例えば`Plugin`トレイトオブジェクトのメモリレイアウトなど）、バージョンが食い違うと正しく動かない可能性があります。

もし任意のRustコンパイラ間で相互運用性を保証する必要があるなら、`Plugin`トレイトのような仕組みは諦めて普通のC-FFIにする必要があるかもしれません。

※コメント欄にC-FFIを使ったバージョンを書いていただきました。ありがとうございます。

# リンク

本記事の派生記事へのリンクを載せておきます。いずれも

# まとめ
これでサンプルの電卓が実装できました。
特にcrate-typeあたりの仕様はドキュメントを読んでもいまいちよく分からなかったので、詳しい方がいたら教えてください。