SBaronda.com a little place I like to call home.


Zig <> Crystal

Apr. 7, 2021

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.

Statically Linked Library

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.

Shared Linked 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.

DLL

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.

Conclusion

A couple different ways of handling loading C-native libaries with Crystal, linking statically against the lib, shared objects, and a DLL styled approach.


comments powered by Disqus