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