⚡Zig💞Rust🦀 DLL InterOP
Tags: rust, zig, abi, windows
Published: 2024-03-30 03:41:07

> This is a translated and recompiled version of two of my Japanese articles on this topic:
> https://zenn.dev/mkpoli/articles/591756f1af6ca8
> https://zenn.dev/mkpoli/articles/4d8c1e28bdd05e

This article demonstrates how to use compiled Windows shared library (*.DLL s) between Zig and Rust through C ABI. Although the focus is primarily on Windows, there would not be much difference on Linux and other platforms.

## Prerequisite

### Installation
#### Rust
See [the official guide of Rust](https://www.rust-lang.org/tools/install)
#### Zig
See [the official guide of Zig](https://ziglang.org/learn/getting-started/#installing-zig)
### TL;DR

```powershell
# Install scoop
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression

# Install Rust
scoop install rustup
rustup update

# Install Zig
scoop bucket add versions
scoop install versions/zig-dev
```

### Development Environment
I'm using [VSCode](https://code.visualstudio.com/). Install the extensions `rust-lang.rust-analyzer` and `ziglang.vscode-zig` (Ctrl+Shift+P, Install Extension). In the Zig confirmation window that appears, set it to the one in the PATH, and install ZLS.

## Load Zig DLL in Rust Application
### Initialize Environment
Execute the following commands to make a folder and open it with VSCode:
```powershell
mkdir zig-rust-interop
cd zig-rust-interop
code .
```
Open the Terminal in VSCode and execute to initialize the projects:
```powershell
mkdir zig-lib
cd zig-lib
zig init
cd ..
mkdir rust-exe
cd rust-exe
cargo init
cd ..
```
Then the following files should be generated:
```
.
├── rust-exe
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── zig-lib
    ├── build.zig
    ├── build.zig.zon
    └── src
        ├── main.zig
        └── root.zig
```

#### Develop Zig library

`zig-lib/src/main.zig` is for generating executables, so we remove it first. We are going to use the generated `zig-lib/src/root.zig`. Let's take a look on it. We can see a simple implementation of `add()` function is `export`ed.

```zig-lib/src/root.zig
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);
}
```

Open `zig-lib/build.zig` and remove the part for executable genration and testing, change `addStaticLibrary`を`addSharedLibrary` as the following:

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

Execute the following command to build the library:
```powershell
cd zig-lib
zig build
cd ..
```

Then inside `zig-lib/zig-out/`, there should be `zig-lib.dll`, `zig-lib.lib` and `zig-lib.pdb` generated. These files have the aforementioned `add()` function embbeded in.

### Develop Rust Application

#### Use `extern "C"` directly

The easiest way to call functions from a dynamic library is to link dynamically in compile time.

Firstly, we add `extern "C"` block in `rust-exe/src/main.rs`.
```rust
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);
}
```

The, create a new file called `rust-exe/build.rs`, and inform the Rust compiler the path to search and library name by `println!()` to `stdout`, then we will copy the generated DLL file to the built target folder (I'm not sure if this is the recommended way, but at least it would be better to let Zig's build script to copy the target to Rust folder because it is too coupled). 

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

Then, if we execute the following command, the Rust application should be compiled and run.

```powershell
cd rust-exe
cargo clean && cargo run
```

However, this methods will not error when the dll file does not exist for some reason (please let me know why), and it is difficult to specify the path dynamically.

#### Use crates such as `libloading`

On the other hand, there is a method to load the DLLs in runtime. Firstly, execute the following command to add [libloading](https://crates.io/crates/libloading) to our dependencies.

```powershell
cargo add libloading
```

Then let's modify `rust-exe/src/main.rs`.

```diff
@@ -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);
 }
```

🎉After that, execute the following command to see the same result of `zig-lib.dll: add(1, 2) = 3` showing.

```powershell
cargo clean && cargo run
```

If we delete `zig-lib.dll`, an error message is shown as below.

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


## Load Rust DLL in Zig Application
### Initialize Environment
Execute the following commands to make a folder and open it with VSCode
```powershell
mkdir zig-rust-interop
cd zig-rust-interop
code .
```
Open the Terminal in VSCode and execute to initialize the projects:
```powershell
mkdir rust-lib
cd rust-lib
cargo init
cd ..
mkdir zig-exe
cd zig-exe
zig init
cd ..
```
Then the following files should be generated:
```
.
├── rust-lib
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── zig-exe
    ├── build.zig
    ├── build.zig.zon
    └── src
        ├── main.zig
        └── root.zig
```

### Develop Rust library

Add `cdylib` to `lib.crate-type` in `Cargo.toml`.

```diff:rust-lib/Cargo.toml
@@ -3,4 +3,7 @@
 version = "0.1.0"
 edition = "2021"

+[lib]
+crate-type = ["cdylib"]
+
 [dependencies]
```

We will use the automatically generated `lib.rs` file. Let's take a look of the content. We can see a simple `add()` function is exported.

```rust
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
```

Add `#[no_mangle]` to the top just before `add()` so that Rust will not mangle with the name with some random characters.

```rust
@@ -1,3 +1,4 @@
+#[no_mangle]
 pub fn add(left: usize, right: usize) -> usize {
     left + right
 }
 ```

Then, after executing the following command, a compiled Rust DLL will be generated.

```powershell
cd rust-lib
cargo build
cd ..
```
```
├── debug
│   ├── build
│   ├── deps
│   │   ├── rust_lib.d
│   │   ├── rust_lib.dll
│   │   ├── rust_lib.dll.exp
│   │   ├── rust_lib.dll.lib
│   │   └── rust_lib.pdb
│   ├── examples
│   ├── incremental
│   │   └── ...
│   ├── rust_lib.d
│   ├── rust_lib.dll
│   ├── rust_lib.dll.exp
│   ├── rust_lib.dll.lib
│   └── rust_lib.pdb
```

### Develop Zig Application

Firstly, rewrite the entry point `main.zig` file as the following:

```zig
const std = @import("std");

pub fn main() !void {
    var dll = try std.DynLib.open("rust_lib.dll");
    const add = dll.lookup(*fn (i32, i32) i32, "add").?;

    std.debug.print("rust_lib.dll: add({}, {}) = {}\n", .{
        1,
        2,
        add(1, 2),
    });
}
```

Then let's remove the part of generating static lib which is not needed for now, and add code to compile Rust in release mode while copying the files back to the build artifact dir `zig-out/bin` as the following:

```zig
const std = @import("std");
const fs = std.fs;

const RUST_DIR = "../rust-lib";
const RUST_RELEASE_DIR = RUST_DIR ++ "/target/release";
const DLL_NAME = "rust_lib.dll";

const RUST_DLL_RELEASE_PATH = RUST_RELEASE_DIR ++ "/" ++ DLL_NAME;
const ZIG_BIN_OUT_DIR = "zig-out/bin";

pub fn build(b: *std.Build) !void {
    _ = b.run(&[_][]const u8{ "cargo", "build", "--manifest-path", RUST_DIR ++ "/Cargo.toml", "--release" });
    const cwd = fs.cwd();
    std.debug.print("Copying {s} to {s}\n", .{ RUST_DLL_RELEASE_PATH, ZIG_BIN_OUT_DIR });
    try fs.Dir.copyFile(cwd, RUST_DLL_RELEASE_PATH, cwd, ZIG_BIN_OUT_DIR ++ "/" ++ DLL_NAME, .{});
    std.debug.print("Copied rust-lib.dll to {s}\n", .{ZIG_BIN_OUT_DIR});

    const target = b.standardTargetOptions(.{});

    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "zig-exe",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}
```

After that, execute the following command:
```powershell
cd zig-exe
zig build run
```

🎉Then `rust_lib.dll: add(1, 2) = 3` should be printed.

## Conclusion

How was the article? Please let me know if you have any more ideas. The working example is published at Github:

https://github.com/mkpoli/zig-rust-interop/tree/master

The reason for this article is that I was trying to make an IME (Input Method Editor) of the Ainu language and I decided to use Zig and Rust for that. It still has a long way to go.