July 29, 2026

C++ libraries linking

When we introduced static linking of C libraries, the promise was simple: name a .lib or .a archive in your #import, compile with -s, and ship one self-contained executable, no DLLs riding along, nothing to install on the target machine.

There was one big frontier left, and everyone saw it coming: the libraries people want most (vision, GUI, audio, machine learning) are written in C++. A C++ library is a very different animal to link: it brings global constructors, exceptions, RTTI, templates, thread-local storage, and an entire language runtime that expects to be wired up just so. Until now, that was the line where you switched back to DLLs.

That line is gone. The Red toolchain now statically links C++ libraries too (their runtime included) on every platform Red targets: Windows, Linux (x86 and ARM), and macOS.

The best part: nothing changes. It is the same import system and same compilation switch:

    red -r -s myapp.red

Libraries built with MSVC, GCC or clang are all accepted, in their native object formats.


What you need preinstalled

Windows: for C++ libraries (or C code built against Microsoft's static runtime), install the free Visual Studio Build Tools with the "Desktop development with C++" workload. Just one installer, and Red locates everything by itself: no vcvarsall, no PATH, no environment variables. Plain C libraries still need nothing at all and that now extends to C libraries touching COM, DirectX or MediaFoundation: the GUID constants such code references ship inside the toolchain, so a fresh Windows 11 with only red-toolchain executable on it links them!

Linux: the GNU runtime archives from your distribution's gcc packages (libstdc++.a, libgcc.a and friends) placed next to your library. If one is missing, the linker names exactly what it needs.

macOS: nothing beyond the toolchain; the system C++ runtime binds automatically.


Some constraints

32-bit libraries for now, until the 64-bit toolchain is ready.

The imported surface must be C (extern "C", cdecl or stdcall), the same contract as any FFI, dynamic linking included. C++ classes and templates do their work inside the library; a thin C shim is required to exposes them to Red (most C++ libraries provide the shim). Keep exceptions inside too: a catch-all guard in each exported function is the pattern. See this wiki page on how to write shims.

No link-time-optimized objects, build libraries without /GL or -flto. Default Release settings are fine.

On Windows, build against the static runtime (/MT).

For comfort, bundle a library and its dependencies into one archive (lib.exe /OUT:foo-red.lib ... on Windows, an ar MRI script elsewhere), so your Red side stays a single #import line.


Examples

We have tested the Red toolchain new linking abilities with some of the most demanding C++ codebases around, built once and run on every platform:

OpenCV5: a complete vision pipeline (filters, features, video decoding) in a single binary, with pixel-identical results on Windows, Linux and a Raspberry Pi.


You can download the C shim code from this direct link. The compiled opencv5c.lib file weights ~270MB. Here is the compilation log for Windows platform:

-=== Red Compiler 0.6.6 ===-

Compiling D:\Dev\opencv5-red\demo.red ...
...GUI backend      : native
...Modules          : View
...compilation time : 1258 ms

Target: Windows

Compiling to native code...
Static linking...
...linking          : opencv5c.lib
...linking (dep)    : libcpmt.lib
...linking (dep)    : libcmt.lib
...MSVC static CRT  : entry -> mainCRTStartup, Red start -> _main
...linking (dep)    : oldnames.lib
...linking (dep)    : uuid.lib
...linking (dep)    : libconcrt.lib
...linking (dep)    : mfuuid.lib
...linking (dep)    : strmiids.lib
...linking (dep)    : comsuppw.lib
...linking (dep)    : libvcruntime.lib
...linking (dep)    : libucrt.lib
...static-link time : 41582 ms
...compilation time : 40670 ms
...global words     : 25805 (78.44%)
...linking time     : 55281 ms
...output file size : 18767872 bytes
...output file      : D:\Dev\opencv5-red\demo.exe 


llama.cpp: building a local LLM chat program in Red gives a ~5 MB standalone executable linking the llama.cpp library: around 40 tokens/s on a desktop, about 2 tokens/s on a RPI4. The C shim is available here (in this case,  just for convenience, it does a bit more than just wrapping the C++ API). The llm-demo code itself is available here and here.

> llm-demo Qwen3-1.7B-Q4_K_M.gguf "Give me one surprising fact about the color red."
model : models\Qwen3-1.7B-Q4_K_M.gguf ctx : 4096 prompt: Give me one surprising fact about the color red. /no_think One surprising fact about the color red is that it is the **only color** that is **not** associated with the word "red" in the English language. While red is
commonly associated with emotions like love, anger, and danger, it is not the word
"red" itself. Instead, the word "red" is derived from the Latin word *rubidus*,
which means "red" or "burning." --- 90 tokens - 40.2 tok/s ---


Dear ImGui: a Dear ImGUI wrapper in Red/System and a full widgets demo running at 60 fps, all linked into a single executable.

You can download the prebuilt signed Windows exe for this demo from this direct link.


Final thoughts

You can use coding agents to generate C shims for any C++ library you need. 

Please report libraries that fail to link in a red/red repo ticket, so we can see if the linker can be improved.

Have fun!

June 29, 2026

Static linking support


We must free ourselves of the hope that the sea will ever rest.
We must learn to sail in high winds.
-Aristotle Onassis


The coding agents revolution is taking the world by storm and we are right in the middle of it. Like most of you, we have experimented with the agent's amazing (and frustrating) capabilities, pondering the role of Red and our vision in that new world. The conclusion is (un)surprisingly clear, Red is still very relevant and will be even more so as we improve it to better work with agents.

In the meantime, here are some treats, starting with expanding our toolchain to support static linking of  libraries written in C, allowing you to distribute single executables with all dependencies packed inside. This work has been done with the heavy assistance of frontier models and local harnesses (Claude Code and Codex).

You might expect that to be a small addition, but a static linker has to read each platform's object format, pull in just the pieces it needs, fold duplicated sections, resolve system symbols and patch relocations by hand, so there was quite a bit of machinery to put together. The reward is the result everyone wants: a single, self-contained binary, with nothing to install beside it.

A simple example

Let's compress some data without shipping a compression library next to our program. For something concrete we will use miniz, a small, MIT-licensed library that implements the well-known zlib and deflate APIs. It is distributed as a single `miniz.c` / `miniz.h` pair, which makes it especially convenient to compile and link.

The first step is to compile miniz into a static library. There are only two things to keep in mind. Red/System currently produces 32-bit code, so the object has to be 32-bit too; and it helps to switch off a couple of compiler extras (C++ exception tables and stack canaries) that would otherwise make the object reference runtime helpers we do not need. On Windows, with MSVC:

    cl /c /MT /GS- /EHs-c- /GR- miniz.c
    lib /out:miniz.lib miniz.obj

Now we map the two functions we need. Notice that the imported name has no extension at all, just `miniz`:

    Red/System [Title: "miniz round-trip]
    
    #import [
        "miniz" cdecl [
            compress: "mz_compress" [
                dst     [byte-ptr!]
                dst-len [int-ptr!]
                src     [byte-ptr!]
                src-len [integer!]
                return: [integer!]
            ]
            uncompress: "mz_uncompress" [
                dst     [byte-ptr!]
                dst-len [int-ptr!]
                src     [byte-ptr!]
                src-len [integer!]
                return: [integer!]
            ]
        ]
    ]

That extension-less name is where it gets interesting: the toolchain resolves it for you, and a single command-line switch decides how. By default, `miniz` resolves to the shared library for the platform (`miniz.dll` on Windows, `libminiz.so` on Linux) so the program links dynamically, exactly as Red/System has always done:

    red -r demo.reds     ; "miniz" resolves to miniz.dll, linked dynamically

Now add `-s`, for *static* (or the longer `--static`), and the very same `miniz` resolves to the static library we built a moment ago instead:

    red -r -s demo.reds  ; "miniz" resolves to miniz.lib, linked statically

This time `mz_compress` and `mz_uncompress` become part of the executable itself. There is no `miniz.dll` to ship next to it, no `PATH` to set up, no installer to write, just a single, self-contained binary (under 50 KB here) that you can copy anywhere and run. The same source file, one extra flag.

Being explicit, when you prefer

The extension-less name is convenient, but you are always free to spell out the extension yourself, in which case the toolchain honors it exactly, and `-s` has no effect on that particular import. This is handy when one program mixes both kinds of linking: a system library you always want dynamic, alongside a helper you always want baked in. For example:

    #import ["user32.dll" stdcall [...]]   ; always dynamic

    #import ["miniz.lib" cdecl  [...]]     ; always static (Windows)
  
    #import ["libminiz.a" cdecl [...]]     ; always static (Linux / macOS)

So you get the best of both: an extension-less name plus `-s` to switch a whole program at once, or explicit extensions for per-library control. Existing code, which already spells out its `.dll` / `.so` / `.dylib`, keeps working unchanged. So those additions to the toolchain are backward-compatible with your current codebases.

Supported targets

Static linking works across all of Red's main native targets:

  • Windows (x86) — COFF objects and libraries (`.obj` / `.lib`)
  • Linux (x86) — ELF objects and archives (`.o` / `.a`)
  • Linux ARM, including the Raspberry Pi — ELF ARM, in both ARM and Thumb-2 code
  • macOS (Intel) — Mach-O objects and archives

Cross-compilation works as usual, so `-t RPi -s` builds a self-contained ARM binary right on your desktop, ready to copy over to a Pi and run.

A real world case: CherryTracker


I wanted to have a real-world case to test and demo our toolchain static linking capabilities, and I ended up building a fully working tool that I'm so happy with, that I decided to release it: CherryTracker.

The recent advancements in coding agents capabilities allowed me to generate, as a side project, something I wanted to code in Red since a long time: a soundtracker-style mod player! It took a lot (dozens) of iterations with the agent to get the polished version I was aiming for. I still had to make many passes to improve the code quality and style. It's not as short and sophisticated as I would like it to be, but it's good enough for an agent.



The source code is on Github. Pre-built binaries for Windows and Linux are available (see the website or the repo). The Windows binary is signed with my personal code signing certificate. A bunch of mods in various formats I have picked up from ModArchive, is also available from the repo. 

CherryTracker is written in Red, using a Draw-based UI, with a Red/System layer for fast data processing and interfacing with the backend libraries:
  • libxmp: used for decoding all the various mod song formats.
  • SDL3: used to provide the audio layer.

32-bit static versions of those libs are provided in the libs folder, for both target OS. The import code is using the extension-less option:

    #import ["libs/libxmp" cdecl [...]]
    #import ["libs/SDL3"   cdecl [...]]

In my local repo, I also placed the shared lib version of those external libraries. Using the dev compilation mode of the Red toolchain (`-c`) and those shared libraries, the agent and I have the ability then to recompile very quickly new versions for testing (using libRedRT), then for release versions, I switch to static linking using `-r -s` compilation options. This approach has proven to be very simple and efficient during the whole work on this application (in my spare time, over a few weeks).

Try it and see what Red is capable of when coupled with coding agents, especially if you are old enough to have been playing mods in the 80s/90s!

A look under the hood

This is a real static linker, not a "concatenate the bytes" trick. It reads the three native object formats, COFF, ELF and Mach-O, through a common interface, and takes care of the parts that make static linking actually work:

Selective archive loading: An archive can hold hundreds of object files; the linker pulls in only the members that resolve a symbol something already references, then follows that dependency chain to its end. Linking against a large `.lib` does *not* give you a large executable.

COMDAT and weak-symbol folding: C++ (and modern C, through `inline`) emits the same template instantiation, inline function or vtable in every translation unit, expecting the linker to keep one copy and discard the rest. The linker tracks those groups and folds the duplicates, along with the relocations that pointed at them.

Full relocation support, per format: Including the awkward ones: the ARM Thumb-2 split immediates and BL/BLX interworking needed for the Pi, and the Mach-O scattered section-difference relocations that optimized switch tables produce.

System symbol resolution: References to libc / libSystem, plus a built-in set of common compiler intrinsics (64-bit division, stack probes and the like), are resolved automatically, so simple programs link without having to drag in the whole C runtime.

In other words, it behaves the way `link.exe`, `ld` and `ld64` do for the subset that Red/System needs, which is exactly what keeps the simple case ("just link this library") simple.

A few things are deliberately left for later. Full C++ runtime support (exceptions, RTTI, the `std::` library) is a much deeper rabbit hole. For now, plain-C APIs (including the C APIs that many C++ libraries expose) are the path to follow. Debug-information passthrough and incremental linking are on the list as well.


Give it a try, and if you run into a library that does not link cleanly, please tell us about it (just open a ticket).  We would love to see what you build with it.

Enjoy!



Fork me on GitHub