Using a static library written in Rust from C and using Zig as a build system

· diesisteintest's blog


Creating the rust library #

Any Rust library setting staticlib as crate-type in Cargo.toml should be possible (I have NOT tried).

For this demo i will wrap the rust hashmap.

lib.rs

 1pub mod foo {
 2    use crate::sys;
 3
 4    #[derive(Debug, Default, Clone)]
 5    pub struct Foo {
 6        map: std::collections::HashMap<String, i64>,
 7    }
 8
 9    impl Foo {
10        pub fn get(&self, key: impl Into<String>) -> i64 {
11            match self.map.get(&key.into()) {
12                Some(v) => *v,
13                _ => 0,
14            }
15        }
16
17        pub fn insert(&mut self, key: impl Into<String>, value: i64) {
18            let _ = self.map.insert(key.into(), value);
19        }
20    }
21
22    #[no_mangle]
23    pub extern "C" fn foo_new() -> sys::foo_t {
24        let foo = Foo::default();
25        return Box::into_raw(Box::new(foo)) as sys::foo_t;
26    }
27
28    #[no_mangle]
29    pub extern "C" fn foo_destroy(foo: sys::foo_t) {
30        if foo.is_null() {
31            panic!()
32        } else {
33            let _ = unsafe { Box::from_raw(foo as *mut Foo) };
34        }
35    }
36
37    #[no_mangle]
38    pub extern "C" fn foo_get(foo: sys::foo_t, key: *const std::ffi::c_char) -> std::ffi::c_longlong {
39        unsafe {
40            let foo = std::mem::ManuallyDrop::new(Box::from_raw(foo as *mut Foo));
41            let key = std::ffi::CStr::from_ptr(key).to_str().unwrap();
42            let value = foo.get(key);
43            return value as std::ffi::c_longlong;
44        }
45    }
46
47    #[no_mangle]
48    pub extern "C" fn foo_insert(
49        foo: sys::foo_t,
50        key: *const std::ffi::c_char,
51        value: std::ffi::c_longlong,
52    ) {
53        unsafe {
54            let mut foo = std::mem::ManuallyDrop::new(Box::from_raw(foo as *mut Foo));
55            let key = std::ffi::CStr::from_ptr(key).to_str().unwrap();
56            foo.insert(key, value as i64);
57        }
58    }
59}
60pub mod sys {
61    #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]
62    pub type foo_t = *mut std::os::raw::c_void;
63}

The c header file looks like this

include/foo/foo.h

 1#pragma once
 2#ifndef LIBFOO_FOO_FOO_H_
 3#ifdef __cplusplus
 4extern "C" {
 5#endif /* __cplusplus */
 6#include <stdint.h>
 7
 8typedef struct foo_s Foo;
 9
10Foo* foo_new(void);
11void foo_destroy(Foo* foo);
12int64_t foo_get(Foo* foo, const char* key);
13int64_t foo_insert(Foo* foo, const char* key, int64_t value);
14
15#ifdef __cplusplus
16}
17#endif /* __cplusplus */
18#endif /* LIBFOO_FOO_FOO_H_ */

and the c code using it looks like this

src/main.c

 1#include "foo/foo.h"
 2#include <stdint.h>
 3#include <stdio.h>
 4
 5int main() {
 6    Foo* foo = foo_new();
 7    foo_insert(foo, "hamburg", 15);
 8    int64_t value = foo_get(foo, "hamburg");
 9    printf("%ld\n", value);
10    foo_destroy(foo);
11
12    return 0;
13}

This can simply be build with a Makefile

 1CFLAGS+=-Iinclude -Os
 2
 3all: exe
 4
 5run: all
 6	./exe
 7
 8target/release/libfoo.a:
 9	cargo b --release
10
11exe: src/main.o target/release/libfoo.a
12	$(CC) -o $@ $^
13
14src/main.o: src/main.c
15	$(CC) $(CFLAGS) -c $^ -o $@
16
17clean:
18	rm src/main.o exe
19
20.PHONY: target/release/libfoo.a

This build the static library on lines 8 & 9 and uses the result archive (collection of object files) when building the exe on lines 11 & 12 the $@ is the current target name i.e exe and $^ are all dependencies as a list i.e. src/main.o target/release/libfoo.a.

Make sucks #

This is a relatively simple makefile (just look at musl's Makefile) and so is relatively easy to understand, but it still makes use of make variables.

Make only uses one core by default, i have actually aliases make to be make -j$(nproc) in my shell.

Zig to the rescue #

As outlined in another blogpost it is relatively simple to use zig as a c build system.

The scary part is running zig build and also compiling rust with cargo build --release.

My first attempt of writing a build.zig looked like this:

 1const std = @import("std");
 2
 3pub fn build(b: *std.Build) void {
 4    const target = b.standardTargetOptions(.{});
 5    const optimize = b.standardOptimizeOption(.{});
 6
 7    const c_exe = b.addExecutable(.{
 8        .name = "c_exe",
 9        .target = target,
10        .optimize = optimize,
11    });
12    c_exe.linkLibC();
13    c_exe.addCSourceFiles(&.{
14        "src/main.c",
15    }, &.{
16        "-Wall",
17        "-Wextra",
18    });
19    c_exe.addIncludePath(.{ .path = "include" });
20    c_exe.addObjectFile(.{ .path = "target/release/libfoo.a" });
21
22    const cargo_build = b.addSystemCommand(&.{ "cargo", "build", "--release" });
23    cargo_build.setName("Build Rust Library(cargo)");
24    b.getInstallStep().dependOn(&cargo_build.step);
25
26    b.installArtifact(c_exe);
27}

It Looks relatively similar, except for the last 3 lines and the c_exe.addObjectFile line.

The c_exe.addObjectFile line ยด simply adds precompiled object files(.o file on Linux) to the build process and also works with .a static libs.

The interesting lines are at the bottom. We add a system command the the build process which run cargo build --release. We also set its name for which gets displayed when running zig build. The last line makes sure that our command is acutally called when building out executable.

cargo build --release compiles the target/release/libfoo.a but even though it is added later to the build system, because c_exe depends on it it is build first.

ERRORS #

This is not working!

When running zig build with this setup you get some link time errors. Looking closely you can see that they all contain Unwind in them. This means some library used by the rust staticlib is missing. I was suprised by this because our Makefile does not link with anything.

From copying one of thos symbol names in Startpage I quickly found, well first I found C++, then some stuff in libgcc and lastly something about libunwind.

I added a naive c_exe.linkSystemLibrary("unwind") to build.zig and it worked. Considiring i first found result for C++ I also tried replacing c_exe.linkLibC with c_exe.linkLibCpp and it also worked.

Sadly none of them work with the target x86_64-linux-musl, so you there is no benefit to either one.

and running ldd zig-out/bin/c_exe does not report more or less dynamic libraries for either one The size is also the same at (on my system) 365K

A repo containing a zig port, all file mentioned here and a Nix flake is available at

https://codeberg.org/tilmanmixyz/rust-from-zig


Copyright (C) 2024 diesisteintest

Codeblocks under 0BSD and Content under CC-BY-SA 4.0