Recently, I have been looking at Zig as a way to build fast C-native libraries for slower languages like Ruby. While I'm familar with Ruby, I wanted to try C-native libraries with another language, like Crystal instead. There are a couple different ways of building a C-native library. We'll explore these methods within Crystal.
zig build-lib lib.zig -OReleaseSmall
This is rather simple method which will produce .a
archive file which contains all the object files.
By default Zig will statically compile this.
❯ nm liblib.a
zig-cache/o/493e44e77fedb0ebdbff08c1d3e209ca/lib.o:
0000000000000000 T add
As we can see we only have one method within this library and it's add
.
crystal build main.cr
With Crystal it's really easy to link against this library.
❯ ldd main
linux-vdso.so.1 (0x00007ffe24570000)
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007fb57818e000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fb577df0000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fb577bd1000)
libevent-2.1.so.6 => /usr/lib/x86_64-linux-gnu/libevent-2.1.so.6 (0x00007fb577980000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fb57777c000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fb577564000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb577173000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb57894b000)
Since liblib.a
is static we don't have any dynamic linking that we need to do. So we don't see liblib
defined in the above output.
./main
50000001
Running it works just as we'd expect. Next up is something is a little more interesting. A shared library.
zig build-lib lib.zig -dynamic -OReleaseSmall
Not too difficult, we just need to pass in -dyanmic
to make Zig create a dyanmic library.
❯ ldd liblib.so
statically linked
❯ file liblib.so
liblib.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, with debug_info, not stripped
Note the file extesion .so
which is a shared object file. This is similar to a .dll
file and it needs to exist on the filesystem after we've linked the application.
crystal build main.cr
❯ ldd main
linux-vdso.so.1 (0x00007ffefdf71000)
liblib.so => not found
libpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f5c17ad8000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f5c1773a000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f5c1751b000)
libevent-2.1.so.6 => /usr/lib/x86_64-linux-gnu/libevent-2.1.so.6 (0x00007f5c172ca000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f5c170c6000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f5c16eae000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5c16abd000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5c18295000)
liblib.so => not found
because liblib.so
is not in the correct library paths. These are defined by ldconfig
and friends.
We need to either move this somewhere that ldconfig
can find it or we can override it.
LD_LIBRARY_PATH=$(pwd) ./main
50000001
Manually setting it to look within the current folder will make this work. And again, the program outputs the expected value.
Wouldn't it be nice if we didn't need to move it to a specific spot or didn't need to use the environment variable? We can by adjusting where the runpath for the application.
crystal build main.cr
Let's build the application.
readelf -d main | ag runpath
Reading the elf file will tell us where the runpath is and what it is currently set to.
By default missing RUNPATH
on my system.
patchelf --set-rpath '$ORIGIN' main
We can patch the elf file to set it to $ORIGIN
which has a special meaning.
❯ readelf -d main | ag runpath
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
❯ ./main
50000001
And checking the elf file to see what it's set to. It's now set to what we'd expect via the patchelf
process.
crystal build main.cr --link-flags -Wl,-rpath=$(pwd)
Above is another method which allows us to set it while building the application instead of after the fact.
❯ readelf -d main | ag runpath
0x000000000000001d (RUNPATH) Library runpath: [/home/silas/coding/crystal/zig-crystal]
❯ ./main
50000001
The problem is the absolute path for the lookup path. This will cause problems if we moved the binary.
crystal build main.cr --link-flags -Wl,-rpath='\$ORIGIN'
Again, by using $ORIGIN
we can set rpath
correctly to be relative to current location of where the application is.
One caveat is that when we move the binary, we'll need to move the .so
as well.
❯ readelf -d main | ag runpath
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
❯ ./main
50000001
This will now make the executable look for the DLL relative to the executable.
A couple different ways of handling loading C-native libaries with Crystal, linking statically against the lib, shared objects, and a DLL styled approach.