Zigで書いたDLLをRustで読み込む
姉妹編
Windows版アイヌ語IMEを作るにあたって、ZigでTSFライブラリを書いてみたいけど、やはり設定画面などGUIはtauriとSvelteKitで作りたいので、とりあえず最初のステップとして(?)RustとZigのインターロップを勉強しておきたいのでやってみました。
はじめに
本稿では、RustのアプリにZigで生成されたDLL(ダイナミック・リンク・ライブラリ)を読み込む方法についてお話します。せっかくなのでリンクする方法と、ロードする方法の両方についてご紹介します。基本的にはWindows中心ですが、Linuxなどでも似たような手順でできるかと思われます。
TL;DR
C ABIを介してやります。ロードする場合はlibloadingを使います。
インストール
TL;DR
(Windows Terminalを入れて)Powershellを開いて
# Scoopをインストール(
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
# Rustをインストール
scoop install rustup
rustup update
# Zigをインストール
scoop bucket add versions
scoop install versions/zig-dev
再起動します。
開発環境
VSCodeを入れます。rust-lang.rust-analyzer及びziglang.vscode-zigという拡張機能をインストールします(Ctrl+Shift+P、Install Extention)。出てきたZigの確認窓でPATHにあるものを設定、ZLSはインストールします。
プロジェクト設定
mkdir zig-rust-interop
cd zig-rust-interop
code .
開いたVSCodeのターミナルを開いて
mkdir zig-lib
cd zig-lib
zig init
cd ..
mkdir rust-exe
cd rust-exe
cargo init
cd ..
すると、以下のようなフォルダー構造になります
.
├── rust-exe
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── zig-lib
    ├── build.zig
    ├── build.zig.zon
    └── src
        ├── main.zig
        └── root.zig
ライブラリの開発(Zig側)
zig-lib/src/main.zigは実行ファイルを生成するためのものなので消します。今回はそのまま自動生成されたzig-lib/src/root.zigを使うので、その中身を覗いてみましょう。簡単な足し算関数add()がexportされているのがわかります。
const std = @import("std");
const testing = std.testing;
export fn add(a: i32, b: i32) i32 {
    return a + b;
}
test "basic add functionality" {
    try testing.expect(add(3, 7) == 10);
}
zig-lib/build.zigを開きます。EXEを生成するため部分やコメントなどを消し、addStaticLibraryをaddSharedLibraryに変えます。変更したものはこうなります。
const std = @import("std");
pub fn build(b: *std.Build) !void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const dll = b.addSharedLibrary(.{
        .name = "zig-lib",
        .root_source_file = .{ .path = "src/root.zig" },
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(dll);
    const lib_unit_tests = b.addTest(.{
        .root_source_file = .{ .path = "src/root.zig" },
        .target = target,
        .optimize = optimize,
    });
    const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_lib_unit_tests.step);
}
ターミナルで以下のコマンドを実行します。
cd zig-lib
zig build
cd ..
するとzig-lib/zig-out/には、zig-lib.dll、zig-lib.lib及びzig-lib.pdbなどのファイルが生成されます。このdllやlibには、zig-lib/src/root.zigに書いてあるようなのadd関数が搭載されています。
ライブラリの取り込み(Rust側)
 直接extern "C"
ダイナミックなライブラリを取り込むもっとも簡単な方法は、コンパイル時にリンクすることです。
まず、rust-exe/src/main.rsにextern "C"ブロックを追加します。
extern "C" {
    fn add(a: i32, b: i32) -> i32;
}
fn main() {
    let a = 1;
    let b = 2;
    let c = unsafe { add(a, b) };
    println!("zig-lib.dll: add({}, {}) = {}", a, b, c);
}
それから、ビルドスクリプトrust-exe/build.rsを作成して、リンクするためにCargoにリンク先のファイルを教えて、さらに生成されたフォルダーにDLLをコピーします[1]。
use std::{env, fs, path::Path};
const LIB_NAME: &str = "zig-lib";
const ZIG_OUT_DIR: &str = "../zig-lib/zig-out/lib";
fn main() {
    println!("cargo:rustc-link-search=native={}", ZIG_OUT_DIR);
    println!("cargo:rustc-link-lib=dylib={}", LIB_NAME);
    let rust_root = env::var("CARGO_MANIFEST_DIR").unwrap();
    let profile = env::var("PROFILE").unwrap();
    let dll_name = format!("{}.dll", LIB_NAME);
    let out_dir = Path::new(&rust_root).join("target").join(&profile);
    let src_path = Path::new(ZIG_OUT_DIR).join(&dll_name);
    let dst_path = Path::new(&out_dir).join(&dll_name);
    if !src_path.exists() {
        panic!(
            "{} not found. Run `cd ../zig-lib && zig build` first.",
            src_path.display()
        );
    }
    fs::copy(&src_path, &dst_path).unwrap();
}
そして、以下のコマンドを実行すると、Rustアプリがコンパイルされ実行されます。
cd rust-exe
cargo clean && cargo run
しかし、この方法ではDLLが当たり前のものとしてありますので、もしdllファイルが存在しないとき、何もエラーが出てきません(本当にそれが原因か?有識者求む)。また、ライブラリは恐らくシステムが探してくる(cwdかPATH)ので、場所の指定も難しいです。
 libloadingなどでロードする
一方、実行中(?)にロードする方法もあります。まず以下のコマンドでlibloadingを依存関係に追加します。
cargo add libloading
それから、rust-exe/src/main.rsを変更します。
@@ -1,10 +1,14 @@
-extern "C" {
-    fn add(a: i32, b: i32) -> i32;
+fn add(a: i32, b: i32) -> Result<i32, Box<dyn std::error::Error>> {
+    unsafe {
+        let lib = libloading::Library::new("zig-lib.dll")?;
+        let add: libloading::Symbol<unsafe extern "C" fn(i32, i32) -> i32> = lib.get(b"add")?;
+        Ok(add(a, b))
+    }
 }
 fn main() {
     let a = 1;
     let b = 2;
-    let c = unsafe { add(a, b) };
+    let c = add(a, b).unwrap();
     println!("zig-lib.dll: add({}, {}) = {}", a, b, c);
 }
それから以下のコマンドを実行中すると、同じく、zig-lib.dll: add(1, 2) = 3が表示されます。
cargo clean && cargo run
もし、zig-lib.dllを削除したなら、以下のようなエラーメッセージが出てくるはずです。
thread 'main' panicked at src\main.rs:12:23:
called `Result::unwrap()` on an `Err` value: LoadLibraryExW { source: Os { code: 126, kind: Uncategorized, message: "Cannot find specified module" } }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
まとめ
以上のように、Zigで書いたDLLライブラリをRustで取り込んでみましたが、いかがだったでしょうか?
まあ目標はまだ遠く役に立つかどうかはわからないのですが、いろいろ研究することでいくつかZigやRustの使い方もわかってきましたので、頑張っていきます。
作動するコードをGithubに公開しました!
- 
これが推奨された方法かどうかはわからりませんが、ただ流石にZigのbuild.zigからRustにコピーするのは結合度が高すぎて利用者の都合知ったことかってなりますし、別途コピーするのにもタスクランナーを必要ですので、Rustのビルド時に必要なアーティファクトを用意する意味では、間違ってないのかもしれません ↩︎
 
Discussion