Friday, October 30, 2026
HomeGolangHow we achieved single-binary static linking with out Cgo (Home windows &...

How we achieved single-binary static linking with out Cgo (Home windows & Linux) – Technical Dialogue


Hello, fellow Gophers!

I’m excited to share a undertaking I’ve been engaged on: a utterly Cgo-free Go binding for the Slint UI engine, presently engaged on each Home windows and Linux (amd64).

For years, constructing native desktop GUIs in Go has compelled builders into tough trade-offs: both pull in a heavy net stack (Chromium/Webview by way of Wails), take care of immediate-mode UI quirks, or wrestle with Cgo dependencies that break quick construct instances and painless cross-compilation.

Slint is a contemporary, declarative GUI toolkit written in Rust with tiny useful resource footprints, stunning reactive DSL, and a royalty-free desktop license for proprietary apps. Nevertheless, bringing a Rust engine into Go with out Cgo appeared practically unattainable at first. Right here is how it’s constructed below the hood.


The Technical Problem: Why Zero-Cgo is Laborious

In commonplace Go improvement, linking overseas native libraries requires Cgo. To get rid of Cgo completely, two main hurdles needed to be solved:

  1. Self-contained Native Static Library
    Cgo can’t be eliminated if the construct course of nonetheless invokes C compiler headers. We bundled all the Slint engine and its C-API wrapper (SlintCFlat) right into a single, precompiled static archive.
  2. **Direct Linking by way of .syso**
    The Go compiler (cmd/compile and cmd/hyperlink) natively acknowledges .syso (System Object) information positioned inside a package deal listing and hyperlinks them straight with out invoking GCC or Clang. However merely dropping a .syso into the package deal doesn’t work out of the field.

The Secret Sauce: Bridging the ABI Hole with Trampolines

The core motive .syso linking fails for overseas features is the ABI mismatch:

  • Go makes use of its personal inside calling conference (traditionally Plan 9 ABI on x86, transitioning to register-based ABI0/ABIInternal).
  • This conference is essentially incompatible with commonplace OS calling conventions (Microsoft x64 ABI on Home windows, System V AMD64 ABI on Linux).
  • Register allocations, stack alignment, and shadow area differ utterly.

To make the Go compiler name into Slint’s C-API straight:

  • Each single API perform requires an meeting trampoline that saves the Go runtime state, aligns the stack in line with the host platform’s ABI, strikes parameters from Go’s registers/stack into host registers (e.g., RCX/RDX/R8/R9 on Home windows or RDI/RSI/RDX/RCX on Linux), calls the overseas image, and marshals return values again to Go.
  • Writing tons of of meeting trampolines by hand throughout a number of OS targets is just about unattainable to take care of.

To resolve this, the trampolines and Go perform stubs are mechanically generated by the Hike language compiler (a toolchain I developed). You don’t have to write down a single line of meeting or C.


The Outcome

By combining pre-packaged .syso archives with compiler-generated meeting trampolines, we obtain:

  • Zero Cgo: CGO_ENABLED=0 builds simply work.(Nevertheless, you need to manually specify the choices that the Slit package deal passes to the linker.)
  • Lightning-fast compilation: On the spot builds utilizing pure Go toolchains.
  • Single static binaries: Slint markup information are merged by way of our bundling device and embedded straight into the binary utilizing go:embed.

Presently, the binding makes use of the Slint dynamic interpreter to judge UI markup at runtime. We hope that someday the Slint workforce will present first-class native Go code technology, however for now, this gives a quick, light-weight, and trendy GUI expertise for Go builders.

Take a look at the repository and examples right here:

Suggestions, questions in regards to the linking internals, and options are very welcome!


“Go perform stubs are mechanically generated by the Hike language compiler (a toolchain I developed).”

Very cool stuff!

It’s additionally fascinating to see within the undertaking that UI occasions are dealt with in form of queues, if I’m not unsuitable (Jogs my memory of io_uring( io_uring – Wikipedia ) however used at a better degree).

Thanks for the type phrases, Akilan!

The io_uring comparability is an fascinating take, however the architectural motive for this express polling loop goes even deeper into how we handle the OS predominant thread.

In commonplace Slint bindings (like JavaScript), occasion dealing with makes use of callbacks the place handler features are invoked implicitly. Whereas handy, that method obscures how the primary thread waits for occasions and turns the primary occasion loop right into a black field.

On this Go binding, we deliberately selected an express occasion polling mannequin:

  1. Deterministic thread execution: Occasion processing is assured to run explicitly on the primary thread (locked by way of runtime.LockOSThread()).
  2. Coexistence with main-thread-only OS assets: Since polling can return or outing when idle, builders can interleave different OS-level operations that strictly require the primary thread (corresponding to Win32 COM/OLE routines, macOS Cocoa loops, or native platform assets) alongside GUI occasion processing.

Giving full management of the primary loop again to the developer is what makes this structure so sturdy for real-world Go purposes!

Thanks lots for taking the time for writing the response! (Very a lot appreciated). The deterministic thread execution is absolutely fascinating.

“builders can interleave different OS-level operations that strictly require the primary thread”

I see, does this imply all the library runs on a single thread ?

Not fairly! It doesn’t limit all the library or your utility to a single thread.

Relatively, solely the UI occasion loop and OS-level windowing interactions are explicitly pinned to the OS predominant thread (by way of runtime.LockOSThread()). That is strictly to fulfill OS necessities (like Win32 message pumps, macOS Cocoa runloops, or GPU context guidelines) the place sure assets should not migrate throughout native threads.

Every thing else in Go behaves utterly usually:

  • You’ll be able to freely spawn background goroutines throughout a number of OS threads for heavy computation, file I/O, or networking.
  • Background goroutines can simply talk with the UI loop utilizing commonplace Go channels or protected synchronization primitives.
  • Slint’s inside engine can even handle its personal background staff or rendering duties the place relevant.

Briefly, it ensures thread-affinity the place the OS calls for it, whereas preserving Go’s full M:N concurrency mannequin intact in all places else!

I see, bought it!

Thanks, Will certainly maintain an eye fixed out on the undertaking for future updates (I’m engaged on UI system for analysing flight information from a number of periods of flight simulations which can be open supply by the mid of subsequent 12 months. I’ve added it to my listing of libraries to doubtlessly use by early Jan 2027).

Hello Akilan,

As a fast replace following our dialogue on direct GPU/vector rendering wants:

To assist advance the Go graphics ecosystem—notably for recreation engines and customized GUI toolkits past Slint—I’ve simply launched WGPUStatic, a Cgo-free static binding of Rust’s wgpu-native for Go:

It permits linking wgpu-native straight into standalone Go executables by way of .syso and Plan 9 meeting stubs with out runtime DLL dependencies (presently examined and operating on Home windows amd64).

Since your flight simulation telemetry and evaluation undertaking will doubtless require high-performance, uncooked GPU rendering pipelines alongside UI widgets, I believed this may be helpful in your architectural issues. Better of luck along with your undertaking!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments