What Color is Your Library (LLM)

2025/09/01

While battling with linking and versioning issues with dynamically loaded shared libraries, it occurred to me that the function coloring metaphor is quite appropriate for shared libraries as well. So, here’s an LLM-generated post in the same style as the original function coloring piece, complete with the prompt that generated it. I found it pretty amusing and even somewhat useful.

In the style of https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/, write an article titled “what color is your library”. It explores the difficulties that shared libraries pose compared to static libraries, touches on how shared libraries are “red” libraries and are contagious, prone to make all other libraries in the project also become shared libraries. If appropriate, also explore alternative solutions in this space like the function article.

You’ve seen this before.

You start with a clean, fast, blue world: one statically linked executable, everything compiled together, LTO humming, symbols private, nothing surprising at runtime.

Then someone says, “We need a plugin.” Or “We’ll ship a shared core so other teams can embed us.” You flip one target to a .so, add -fPIC a couple places, and—whoops—the color bleeds. Your build turns red.

Shared libraries are red libraries. They’re contagious. Once one shows up, it wants your whole graph.


Blue vs. red (static vs. shared)

Both have their place. But red brings rules that blue code never had to obey—and those rules spread.


Why red spreads

1) PIC tax is transitive

On ELF platforms, if a shared object depends on static archives, those archives must have been compiled with PIC (e.g., -fPIC or -fpic). If they weren’t, you hit errors like:

relocation R_X86_64_32 against `.text’ can not be used when making a shared object; recompile with -fPIC

So your brand-new libcore.so can’t consume yesterday’s comfortable libalgo.a unless you rebuild libalgo with PIC. And if libalgo depends on libutil.a, that needs PIC too. One red target forces a red rebuild of its entire static closure.

People try to dodge by “just link the .a into the final binary and dlopen the .so separately.” That only works if the shared object doesn’t pull on those .as. The moment it does, you’re back to repainting.

2) ABIs harden at boundaries

Red turns implementation details into promises:

The fix is discipline—visibility hygiene, C-shaped APIs, versioned symbols—but that’s work. And once one boundary exists, you need that discipline everywhere that boundary touches.

3) The loader is now part of your system

The dynamic loader resolves search paths and symbol bindings based on DT_NEEDED, RPATH/RUNPATH, LD_LIBRARY_PATH, and dlopen flags (RTLD_LOCAL/RTLD_GLOBAL). The order you load things can change which functions get called. $ORIGIN becomes a design decision, not an afterthought.

If your program was blue yesterday, nobody on the team internalized these rules. Introduce one .so, and every deploy script, container, and CI job now has to care.

4) Performance shifts

Modern x86-64 PIC overhead is usually modest, but it’s not zero—extra indirections through GOT/PLT, fewer cross-module inlines, more conservative devirtualization. Startup relocations and symbol lookups show up in tail latency. If your world is ultra-low-latency, these details matter. Turning one central library red pushes PIC and PLT decisions across everything that touches it.


Why people choose red anyway

All valid. Just don’t pretend the rest of the code can stay innocent.


A field guide to red contagion

Symptoms you’ll see after introducing one .so:

Congrats: your project is red now.


Surviving in a red world

If you must be red, be intentionally red.

  1. Draw narrow, explicit, C-shaped boundaries
  1. Hide everything else
  1. Tame the loader
  1. Control performance

Alternatives (choose your palette up front)

A) Stay purely blue (static all the way)

When to pick: servers you own end-to-end, no third-party plugins, strong perf/repro needs.

B) Red islands on a blue sea

Keep the application and internal libs static. Introduce one red boundary for plugins:

This keeps most of the graph blue and confines red rules to a small, well-documented island.

C) Separate process instead of .so

Use IPC (Unix domain sockets, shared memory ring buffers, memfd) to compose. You pay a crossing cost, but you get:

D) “Ship shared, build blue”

Internally build statically to keep sanity and performance. At publish time, produce a red distribution by carving out a few shared components with crystal-clear ABIs. Treat that split as a release-engineering step, not how developers work day-to-day.

E) Header-only or static-preferred dependencies

For third-party libs under your control, prefer header-only or static builds. If you must depend on a vendor .so, put a thin adapter around it and keep the rest blue.


Practical build hygiene (CMake/Bazel-ish)


The moral

Color is a property that propagates.

If you choose blue, protect it: say no to casual plugins and enforce a static-first toolchain. If you choose red, embrace it: draw tight C ABIs, hide your guts, police exports, and script the loader.

What doesn’t work is pretending a red decision can live quietly in a blue world. It won’t. One shared library invites the loader, ABI stability, PIC, and interposition to your party—and they don’t mingle politely.

Pick your color early. Make the boundary small. And whatever you do, don’t let accidental red repaint the whole house.