Skip to content
academia.sh

Lesson 12 / 15

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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
// 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.

// 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
// 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.

// 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;
}
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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close