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
Zig
TL;DR
# 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. 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:
mkdir zig-rust-interop
cd zig-rust-interop
code .
Open the Terminal in VSCode and execute to initialize the projects:
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.
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:
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:
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
.
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).
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.
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 to our dependencies.
cargo add libloading
Then let's modify 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);
}
πAfter that, execute the following command to see the same result of zig-lib.dll: add(1, 2) = 3
showing.
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
mkdir zig-rust-interop
cd zig-rust-interop
code .
Open the Terminal in VSCode and execute to initialize the projects:
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
.
@@ -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.
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.
@@ -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.
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:
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:
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:
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.
Top comments (0)