https://karnwong.me/posts/rss.xml

Static lib linking in Go program

2025-08-26

Background

I have a Go program for various utilities. Some of the features talk to hardware: sensors and battery statistics. For funsies, I ported this program into Rust because I want to get a feel of the language outside the data/ml ecosystem. To my surprise, I found that Rust's implementation of these features have better hardware coverage. Specifically, in Go it couldn't get sensors info on some hardware, and battery cycle count isn't available in Go at all.

I like Rust, but I find Go has better TUI ecosystem. As to not shoot my future-self in the foot, utilizing FFI might be more ideal.

How it works

Basically you have to use Rust to build a static library, then import it into Go. Finally, bundle this static lib into a Go binary.

Note that you have to expose the Rust functions for C, and you also need to provide a C header file.

Regarding static vs dynamic library: you probably have an easier time linking a dynamic binary, but it's more headaches if you want to distribute a plain binary. This is because for dynamic library, the library itself has to be present somewhere on your system so the Go binary can load external library at runtime.

Since a binary is OS/arch dependent, so is static library. This is where the fun starts. Good news is you can use cross to cross-compile Rust crates, but this doesn't apply to actually linking the static lib into Go binary.

CGO import

In Go, you need to set CGO import:

/*
#cgo linux LDFLAGS: ./lib/libsystem.a
#cgo darwin LDFLAGS: ./lib/libsystem.a -framework IOKit
#cgo windows LDFLAGS: ./lib/libsystem.a
#include "../../lib/system.h"
#include <stdlib.h>
*/
import "C"

Notice that you probably need different configurations for each OS. Notice that for darwin, framework is required since it's MacOS specific. I got the error during build and it pointed me to which framework to specify for LDFLAGS.

Go build configurations

You also need to set the right combination for C Compiler and LDFLAGS, in which it is as follows:

OSArchCCLDFLAGS
linuxamd64-linkmode 'external' -extldflags '-static'
linuxarm64aarch64-linux-gnu-gcc-linkmode 'external' -extldflags '-static'
darwinamd64-linkmode 'external'
darwinarm64-linkmode 'external'
windowsamd64x86_64-w64-mingw32-gcc-linkmode 'external' -extldflags '-static -lntdll -lsetupapi'

Notice the lntdll flag for windows, during initial binary build I didn't add this and it throws an error, pointing me to add this flag for compilation.

Note that linux can cross-compile for windows, but darwin needs a MacOS machine.

Also it might throw an error about recommending musl. Because gcc-aarch64-linux-gnu and gcc-mingw-w64-x86-64 depend on glibc. To fix this error, you have to use a musl-based C Compiler instead. But this entirely depends on whether your static library talks to glibc or not, you don't have much say in this.

CI/CD

You should utilize a matrix strategy to fan out the build jobs, and don't forget to install the C Compiler and Rust target for the correct target OS/arch combination.

Relevant snippets:

- name: Install cross-compilers
  run: |
    if [ "${{ matrix.os }}" == "linux" ]; then
      sudo apt-get install gcc-aarch64-linux-gnu
    elif [ "${{ matrix.os }}" == "windows" ]; then
      sudo apt-get install gcc-mingw-w64-x86-64
    fi
- name: Add Rust targets
  run: |
    if [ "${{ matrix.os }}" == "darwin" ]; then
      rustup target add x86_64-apple-darwin
      rustup target add aarch64-apple-darwin
    elif [ "${{ matrix.os }}" == "windows" ]; then
      rustup target add x86_64-pc-windows-gnu
    fi