---
title: 'Building Libraries'
source: 'https://academia.sh/en/courses/c-systems-programming/building-libraries'
course: 'Systems Programming with C'
language: en
updated: '2026-09-26T03:02:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures that embedded binding copies the library's value while pointer-based binding shares it"
    - 'Shows why static and extern scope bind a same-named variable to a separate object or a single one'
    - 'Distinguishes the two within the same measurement framework using the copy and shared classes'
---

# Building Libraries

Measures how whether a library is bound into the program by embedding or read at load time decides which value the program sees when the library's version changes.

Once a target is correctly compiled into an object, it doesn't run on its own — most programs
link against functions and data that other units produce. This linking can happen in two
different ways: the library's current value is **copied** into the program's own body, or the
program reaches the library through a **pointer** and reads the current value every time. When
the library is later updated, these two programs see something different from each other.

The same distinction plays out between a variable that never opens beyond a single translation
unit and one that does. A `static` file-scope variable gives every translation unit its own
copy; a variable shared through an `extern` declaration has exactly one definition, and every
unit that uses it sees the same object.

This lesson brings back the previous lesson's **copy** class at a different layer. There, copy
was the stale value carried by a record that was never written into the dependency list; here,
copy is a library version taken at bind time and never updated again. Both produce the same
symptom — a value older than expected — but one arises in the build system's decision layer,
the other in the linking layer. Being able to tell these two layers apart is the first step in
narrowing down where to look when hunting a bug.

## Embedded Version and Pointer-Based Access

The model below defines a library's version and two structures that bind to it through two
different paths: one copies the version into its own field at bind time, the other stores the
pointer leading to the library.

```c
// binding.h — version visibility of embedded and pointer-based binding
#ifndef BINDING_H
#define BINDING_H

struct Library {
    int version;
};

struct EmbeddedBinding {
    int embedded_version;   /* value copied at bind time, independent afterward */
};

struct PointerBinding {
    const struct Library *target;   /* not a value, but the path to the library is stored */
};

void embedded_bind(struct EmbeddedBinding *g, const struct Library *lib);
int embedded_version(const struct EmbeddedBinding *g);

void pointer_bind(struct PointerBinding *b, const struct Library *lib);
int pointer_version(const struct PointerBinding *b);

#endif
```

```c
// binding.c — embedded binding copies, pointer binding rereads on every read
#include "binding.h"

void embedded_bind(struct EmbeddedBinding *g, const struct Library *lib) {
    g->embedded_version = lib->version;
}

int embedded_version(const struct EmbeddedBinding *g) {
    return g->embedded_version;
}

void pointer_bind(struct PointerBinding *b, const struct Library *lib) {
    b->target = lib;
}

int pointer_version(const struct PointerBinding *b) {
    return b->target->version;
}
```

**PI3.** `embedded_bind` here stands in for the embedding a real program performs at compile
time: the library's current value behaves as if it were fixed before the program even starts
running. In a real build system this step happens during compilation; here it's modeled with a
copy operation at runtime, to make the timing visible.

These two structures correspond to two ways of binding a library in a real build environment.
`EmbeddedBinding` represents a **statically linked** library: the library's compiled code is
copied directly into the program's own executable, and the program never looks at the library
again. Even if the library is later updated and recompiled, the copy frozen into the statically
linked program's body doesn't change — unless the program itself is relinked, the updated
version never reaches it at all. `PointerBinding`, on the other hand, represents a **shared**
(dynamically) linked library: the executable doesn't carry the library's own code, it only holds
a path leading to it and goes to the library on every access. When the library is updated and
rebuilt, the program itself sees the updated version on its next run without ever being
recompiled — because the read is done from the library itself, not from a copy embedded in the
program's body.

**PI4.** The real-world difference between static and shared linking lies in load timing and in
the existence of a separate library file on disk; this model represents that timing difference
within a single runtime flow, by making the `embedded_bind` call happen before the library is
updated. The measured outcome — the statically linked one staying stale, the shared one seeing
the update — arises not from the timing itself but from the difference between copying and
holding a reference, so the model skipping the real loading mechanism doesn't change the result.

## static and extern Scope

When a library is split across several translation units, the difference between counters
private to each unit's own file and a single counter shared by all units can be measured in the
same framework.

This contrast is set up **in the header**, not in the source file: declaring a variable as
`extern` in a header like `shared.h`, included by several source files, promises that the
variable will have exactly one definition and that every unit including it will see the **same
object**. Writing a `static` declaration into the same header couldn't make that promise —
`static` confines its scope directly to the translation unit it appears in; if it were written
into a header and included by every source file, each including unit would produce its own
**separate** copy, not a single shared object. So if sharing is wanted, the declaration must be
`extern` in the header and the definition must sit in exactly one source file; if keeping it
private to a unit is wanted, the declaration must stay `static` in the source file itself and
never move into a header at all.

Getting this distinction wrong has two different consequences. If a variable that needs to be
shared is mistakenly left `static` — for instance, if no `extern` declaration is ever added to
the header and every source file is left to define its own copy — the units keep separate
counters, unaware of one another, and a shared total like `shared_counter` never comes into
existence; each unit believes the partial count it sees is the correct one. The opposite mistake
is also possible: if a variable with the same name is defined directly, without `extern`, in
more than one source file — that is, if each source file tries on its own to define that name as
if it were a shared object — the linker finds the same name defined in more than one place, and
this usually ends in a link error. The `extern` declaration in the header prevents this second
mistake too, because it gives every unit that includes the header only a **declaration**, not a
**definition**; the definition stays in exactly one source file.

```c
// shared.h — the declaration that exposes the shared counter
#ifndef SHARED_H
#define SHARED_H

extern int shared_counter;    /* extern: one definition, every unit shares the same object */

void state_increment(void);
int state_count(void);

#endif
```

```c
// state.c — the static counter is private to this unit, the extern counter is shared
#include "shared.h"

static int unit_counter = 0;   /* static: a copy private to this translation unit */
int shared_counter = 0;         /* the extern declaration's one definition is here */

void state_increment(void) {
    unit_counter++;
    shared_counter++;
}

int state_count(void) {
    return unit_counter;
}
```

The unit containing `main` also keeps its own `static` counter; even though its name is the
same as the one in `state.c`, `static` scope makes it a separate object, and it doesn't break
compilation.

```c
// measurement.c — embedded/pointer binding and static/extern scope in a single run
#include <stdio.h>
#include "binding.h"
#include "shared.h"

static int unit_counter = 0;   /* private to this unit; separate from the same-named variable in state.c */

int main(void) {
    struct Library library = { 1 };
    struct EmbeddedBinding embedded;
    struct PointerBinding pointer_bound;

    embedded_bind(&embedded, &library);
    pointer_bind(&pointer_bound, &library);

    library.version = 2;   /* library moved on to version 2 after binding */

    printf("embedded binding version: %d\n", embedded_version(&embedded));
    printf("pointer binding version: %d\n", pointer_version(&pointer_bound));

    state_increment();
    state_increment();
    unit_counter++;
    shared_counter++;

    printf("state.c unit counter: %d\n", state_count());
    printf("main unit counter: %d\n", unit_counter);
    printf("shared counter: %d\n", shared_counter);
    return 0;
}
```

```text
embedded binding version: 1
pointer binding version: 2
state.c unit counter: 2
main unit counter: 1
shared counter: 3
```

The `embedded` structure stayed at the value of 1 it read at bind time — it's unaware that the
library later moved on to version 2, because what it holds is a **copy**. The `pointer_bound`
structure, on the other hand, read the current value because it stores the path to the same
library object; this is a model of the **shared** visibility that stands opposite embedded
binding — this lesson itself doesn't measure it, it's here only for comparison.

These two results mirror, one for one, the response static and shared linking give to a library
update. `embedded` models the behavior of a statically linked program: the library moved on to
version 2, but the program had already embedded a copy of version 1 into its own body, and that
copy never changed again — the only way for it to see the library's new version is for the
program itself to be rebuilt by **relinking** against the library. `pointer_bound` models the
behavior of a shared-linked program: the program itself was never recompiled, but because it
reads the path to the library and sees the library's **current** state on every read, it caught
the update automatically. This difference sums up the fundamental trade-off to weigh when
choosing between the two linking styles: static linking is predictable and immune to the library
changing out from under it, but it requires relinking to pick up an update; shared linking picks
up the update automatically, but the price of that automation is that when the library changes
is outside the program's own control.

The `static` counter in the `state.c` unit counted only its own calls and stayed at 2; the
same-named `static` counter in the `main` unit is entirely independent of it and carries its own
value of 1. `shared_counter`, on the other hand, is the single object both units increment, so
it reached 3, the sum of both.

The same `static`/`extern` distinction holds for functions too; this lesson only shows it
through a variable because the visibility of a copied value is more directly traceable there. If
a function is declared `static`, it can be called only from its own translation unit; another
`static` function with the same name can be defined in a different unit, and the two never
collide, because the linker doesn't see either of them as an externally visible symbol. This is
a cheap way of preventing helper function names from clashing in a large codebase: every helper
that needs to stay private to a unit is left `static`, and only functions that genuinely need to
be shared are moved into the header with an `extern` declaration (or without one, since
functions default to `extern` in C). If this choice isn't made early, undoing it later isn't
cheap: a function first spreads as shared, other units start calling it, and if it's then meant
to be closed back into its own unit, every place that calls it now has to change too — which is
why the visibility decision is made at the moment the library's interface is first drawn up, not
afterward.

## The Bounding Measurement

This model doesn't measure how a library is actually loaded, only **which value** the two
binding styles reach. In a real environment, embedded binding is set up at compile time and
pointer-based binding at program startup; here both are modeled within a single runtime flow,
with their timing made visible. If the library's interface — that is, the order and types of the
fields inside `struct Library` — changes between the two sides, pointer-based binding reading
the current rather than the stale value is no longer a guarantee; this is a boundary that opens
onto undefined behavior, and it isn't run here.

The real-world consequence of static versus shared linking usually shows up in how a security or
bug fix is distributed. When a bug is fixed in one version of a library and the library is
rebuilt, a shared-linked program picks up that fix on its next run without being touched at all
— the fix ships together with the library. A statically linked program doesn't get the same fix;
the old code it embedded into its own body keeps running, and for the fix to reach it, the
program itself has to be relinked against the library and redistributed. This shows that the
choice between the two linking styles isn't only a matter of performance or file size, but also
a matter of maintenance and distribution responsibility: shared linking broadcasts an update
from a single central point (the library itself), while static linking requires every program's
own copy to be updated separately.

The previous lesson's question was whether a dependency was **written down** or not; this lesson
shows that even when a dependency is correctly written down, **how** it is bound to changes
visibility — the two questions complement each other, they don't stop at the same stage.

These two questions can offer two different explanations for the same bug. If a program
unexpectedly carries a stale value, the first thing checked is whether the dependency is written
into the build system's list; if the list is correct but the value still stays stale, the second
question comes into play — is the binding style embedded or pointer-based? An embeddedly bound
value can carry a stale copy even of a dependency correctly written into the list, because the
copying already happened at bind time, before the library changed. These two causes produce a
similar-looking result — in both cases the program sees a stale value — but their sources are
different and they call for different fixes: one wants the dependency list completed, the other
wants the binding style converted to pointer-based.

A practical way to tell these two causes apart is to change the library and rerun the program
**without recompiling** it. If the binding is embedded, the change in the library makes no
difference at all — the program still carries the old, embedded value, because the copying
finished at compile time and a later change to the library can never reach the copy in the
program's memory. If the binding is pointer-based, the same step reveals the new value right
away, because the program reaches the library again every time. This single step is different
from repeating the missing-from-the-list problem: in the missing-from-the-list problem the
library is never read correctly at all; in the binding-style problem the library is read
correctly, but the moment of reading falls at the wrong time.

A third consequence of static versus shared linking shows up when the library's interface
changes. If the field order or type of a structure like `struct Library` changes, a statically
linked program can stay unaware of the change because it embedded a copy of the old interface
into its own body; it doesn't notice the fault as long as it isn't recompiled, but when it is
recompiled, it errors out at compile time and the problem becomes visible. A shared-linked
program, on the other hand, can meet the interface change not at compile time but at runtime —
if the library is rebuilt with the new interface and the program's body is left unchanged, the
program expects the old interface while the library offers the new one, and this mismatch shows
up as a runtime error rather than a compile error. The name for this difference is **interface
compatibility**: in static linking, incompatibility stops early and loudly; in shared linking, it
can go on late and silently.

## Summary

- Embedded binding copies the library's value at bind time; even if the library later
  changes, the program keeps carrying the old value — the class this lesson measures is
  **copy**.
- Pointer-based binding stores the path to the library and sees the current value on
  every read; this is a model of the shared visibility that stands opposite embedded
  binding.
- The `static`/`extern` contrast is set up in the header: if sharing is wanted, the
  declaration must be `extern` in the header and the definition must sit in exactly one
  source file; if keeping something private to a unit is wanted, the declaration must
  stay `static` in the source file without ever moving into a header — `static` gives
  every including unit its own copy, `extern` binds to a single shared object.
- If the library's interface disagrees between the two sides, pointer-based binding
  reading the current value safely is no longer guaranteed.
- Static linking requires the program to be relinked to pick up an update; shared
  linking distributes the update centrally from the library itself.

## Next Step

Knowing that a library is correctly bound doesn't show that the code using it works correctly.
The next lesson looks at how testing a unit by **isolating** it from the rest of the program
measures whether a change leaks into another test.
