Static and Shared Libraries
Libraries let you package compiled code for reuse without distributing source. The distinction between static and shared libraries affects binary size, load time, memory usage, and update strategy. This chapter covers both, plus how to bridge C and Rust libraries across the language boundary.
Static Libraries (.a)
A static library is an archive of object files. At link time, the linker copies the needed object code directly into the final executable.
Creating a Static Library in C
/* vec2.h */
#ifndef VEC2_H
#define VEC2_H
typedef struct {
double x;
double y;
} Vec2;
Vec2 vec2_add(Vec2 a, Vec2 b);
Vec2 vec2_scale(Vec2 v, double s);
double vec2_dot(Vec2 a, Vec2 b);
#endif
/* vec2.c */
#include "vec2.h"
Vec2 vec2_add(Vec2 a, Vec2 b) {
return (Vec2){ a.x + b.x, a.y + b.y };
}
Vec2 vec2_scale(Vec2 v, double s) {
return (Vec2){ v.x * s, v.y * s };
}
double vec2_dot(Vec2 a, Vec2 b) {
return a.x * b.x + a.y * b.y;
}
Build the static library:
gcc -c -O2 vec2.c -o vec2.o
ar rcs libvec2.a vec2.o
aris the archiver.rinserts files into the archive (replacing if they exist).ccreates the archive if it does not exist.swrites an index (equivalent to runningranlib).
Inspect it:
ar t libvec2.a # list contents
nm libvec2.a # list symbols
Linking Against a Static Library
/* main.c */
#include <stdio.h>
#include "vec2.h"
int main(void) {
Vec2 a = {1.0, 2.0};
Vec2 b = {3.0, 4.0};
Vec2 sum = vec2_add(a, b);
printf("sum = (%.1f, %.1f)\n", sum.x, sum.y);
double d = vec2_dot(a, b);
printf("dot = %.1f\n", d);
Vec2 scaled = vec2_scale(a, 3.0);
printf("scaled = (%.1f, %.1f)\n", scaled.x, scaled.y);
return 0;
}
gcc -O2 main.c -L. -lvec2 -o vectest
./vectest
-L.tells the linker to search the current directory for libraries.-lvec2tells it to look forlibvec2.a(orlibvec2.so).
The resulting binary is self-contained -- it does not need libvec2.a at
runtime.
+-------------------+ +-------------------+
| main.o | | libvec2.a |
| main() [T] | | vec2.o: |
| vec2_add [U] ---+------>| vec2_add [T] |
| vec2_dot [U] ---+------>| vec2_dot [T] |
+-------------------+ +-------------------+
\ /
\ /
v v
+---------------------------+
| vectest (executable) |
| main() |
| vec2_add() (copied in) |
| vec2_dot() (copied in) |
+---------------------------+
Caution: Static linking copies code into every executable that uses it. If ten programs link
libvec2.a, each gets its own copy. Security patches to the library require recompiling all ten programs.
Shared Libraries (.so)
A shared library is loaded at runtime. Multiple programs can share a single copy in memory.
Creating a Shared Library
gcc -c -O2 -fPIC vec2.c -o vec2_pic.o
gcc -shared -o libvec2.so vec2_pic.o
-fPICgenerates position-independent code, required for shared libs.-sharedtells the linker to produce a shared object.
Linking Against a Shared Library
gcc -O2 main.c -L. -lvec2 -o vectest_shared
But running it may fail:
./vectest_shared
# error: libvec2.so: cannot open shared object file
The dynamic linker does not search the current directory by default. Solutions:
# Option 1: Set LD_LIBRARY_PATH
LD_LIBRARY_PATH=. ./vectest_shared
# Option 2: Install to a system path
sudo cp libvec2.so /usr/local/lib/
sudo ldconfig
# Option 3: Embed the path at link time
gcc -O2 main.c -L. -lvec2 -Wl,-rpath,'$ORIGIN' -o vectest_shared
The -Wl,-rpath,'$ORIGIN' approach embeds a relative search path in the
binary itself. $ORIGIN expands to the directory containing the executable.
Runtime vs. Compile Time
+-----------------------+
| Compile/Link Time |
|-----------------------|
| gcc finds libvec2.so |
| records dependency |
| does NOT copy code |
+-----------------------+
|
v
+-----------------------+
| Runtime |
|-----------------------|
| ld.so loads .so |
| maps into memory |
| resolves symbols |
+-----------------------+
Check what shared libraries an executable needs:
ldd vectest_shared
Soname Versioning
Shared libraries use a versioning scheme:
libvec2.so.1.2.3 # real name (major.minor.patch)
libvec2.so.1 # soname (major version)
libvec2.so # linker name (symlink)
gcc -shared -Wl,-soname,libvec2.so.1 -o libvec2.so.1.0.0 vec2_pic.o
ln -s libvec2.so.1.0.0 libvec2.so.1
ln -s libvec2.so.1 libvec2.so
The executable records the soname (libvec2.so.1), not the full version.
This means you can update libvec2.so.1.0.0 to libvec2.so.1.1.0 without
relinking executables, as long as the ABI is compatible.
readelf -d vectest_shared | grep NEEDED
# 0x0000000000000001 (NEEDED) Shared library: [libvec2.so.1]
The ldconfig command manages the soname symlinks system-wide. Run
sudo ldconfig after installing a new library to update the cache.
dlopen / dlsym: Runtime Loading
Sometimes you need to load a library at runtime -- for plugins, optional features, or late binding.
Define a plugin with a clean ABI:
/* plugin_api.h */
#ifndef PLUGIN_API_H
#define PLUGIN_API_H
int plugin_init(void);
int plugin_process(int input);
void plugin_cleanup(void);
#endif
/* my_plugin.c */
#include <stdio.h>
#include "plugin_api.h"
int plugin_init(void) {
printf("[plugin] initialized\n");
return 0;
}
int plugin_process(int input) {
return input * 3 + 1;
}
void plugin_cleanup(void) {
printf("[plugin] cleaned up\n");
}
gcc -shared -fPIC -o my_plugin.so my_plugin.c
/* host.c */
#include <stdio.h>
#include <dlfcn.h>
typedef int (*init_fn)(void);
typedef int (*process_fn)(int);
typedef void (*cleanup_fn)(void);
int main(int argc, char *argv[]) {
const char *plugin_path = (argc > 1) ? argv[1] : "./my_plugin.so";
void *handle = dlopen(plugin_path, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
return 1;
}
init_fn init = (init_fn)dlsym(handle, "plugin_init");
process_fn process = (process_fn)dlsym(handle, "plugin_process");
cleanup_fn cleanup = (cleanup_fn)dlsym(handle, "plugin_cleanup");
if (!init || !process || !cleanup) {
fprintf(stderr, "dlsym: %s\n", dlerror());
dlclose(handle);
return 1;
}
init();
printf("process(10) = %d\n", process(10));
cleanup();
dlclose(handle);
return 0;
}
gcc -o host host.c -ldl
./host ./my_plugin.so
Output:
[plugin] initialized
process(10) = 31
[plugin] cleaned up
Link with -ldl to get dlopen / dlsym / dlclose.
Driver Prep: The Linux kernel's module system is conceptually similar to
dlopen. When you runinsmod mydriver.ko, the kernel loads the module's ELF object, resolves symbols against the kernel's exported symbol table, and calls the module's init function.
Rust Library Types
Rust supports several library output types, configured in Cargo.toml:
[lib]
crate-type = ["rlib"] # default: Rust-native library
# crate-type = ["staticlib"] # C-compatible static library (.a)
# crate-type = ["cdylib"] # C-compatible shared library (.so)
# crate-type = ["dylib"] # Rust-native shared library
| Type | File | Use case |
|---|---|---|
rlib | .rlib | Dependency for other Rust crates |
staticlib | .a | Link into a C/C++ program |
cdylib | .so | Shared lib callable from C |
dylib | .so | Shared lib for other Rust code |
Building a Rust Static Library for C
# Cargo.toml
[package]
name = "rustmath"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
#![allow(unused)] fn main() { // src/lib.rs use std::os::raw::c_int; #[no_mangle] pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int { a + b } #[no_mangle] pub extern "C" fn rust_factorial(n: c_int) -> c_int { if n <= 1 { 1 } else { n * rust_factorial(n - 1) } } }
cargo build --release
ls target/release/librustmath.a
Now call it from C:
/* use_rustlib.c */
#include <stdio.h>
#include <stdint.h>
/* Declarations matching Rust's extern "C" functions */
int32_t rust_add(int32_t a, int32_t b);
int32_t rust_factorial(int32_t n);
int main(void) {
printf("rust_add(10, 20) = %d\n", rust_add(10, 20));
printf("rust_factorial(6) = %d\n", rust_factorial(6));
return 0;
}
gcc -O2 use_rustlib.c -L target/release -lrustmath -lpthread -ldl -lm -o use_rustlib
./use_rustlib
The extra -lpthread -ldl -lm flags are needed because Rust's standard
library depends on them.
Rust Note: When producing a
staticlib, Rust statically links its own standard library into the.afile. This makes the archive self-contained but larger. Acdylibdynamically links the Rust standard library.
Building a Rust Shared Library for C
Change the crate type to ["cdylib"] and rebuild. This produces a
librustmath.so that can be dynamically linked from C the same way.
Writing a C Library Callable from Rust
The reverse direction: wrap an existing C library for use in Rust.
/* cstack.h */
#ifndef CSTACK_H
#define CSTACK_H
#include <stdint.h>
#include <stdbool.h>
#define STACK_CAPACITY 64
typedef struct {
int32_t data[STACK_CAPACITY];
int32_t top;
} Stack;
void stack_init(Stack *s);
bool stack_push(Stack *s, int32_t value);
bool stack_pop(Stack *s, int32_t *out);
int32_t stack_size(const Stack *s);
#endif
/* cstack.c */
#include "cstack.h"
void stack_init(Stack *s) {
s->top = -1;
}
bool stack_push(Stack *s, int32_t value) {
if (s->top >= STACK_CAPACITY - 1) return false;
s->data[++(s->top)] = value;
return true;
}
bool stack_pop(Stack *s, int32_t *out) {
if (s->top < 0) return false;
*out = s->data[(s->top)--];
return true;
}
int32_t stack_size(const Stack *s) {
return s->top + 1;
}
Use it from Rust with the cc crate:
# Cargo.toml
[package]
name = "use-cstack"
version = "0.1.0"
edition = "2021"
[build-dependencies]
cc = "1"
// build.rs fn main() { cc::Build::new() .file("cstack.c") .compile("cstack"); }
// src/main.rs use std::os::raw::c_int; const STACK_CAPACITY: usize = 64; #[repr(C)] struct Stack { data: [c_int; STACK_CAPACITY], top: c_int, } extern "C" { fn stack_init(s: *mut Stack); fn stack_push(s: *mut Stack, value: c_int) -> bool; fn stack_pop(s: *mut Stack, out: *mut c_int) -> bool; fn stack_size(s: *const Stack) -> c_int; } fn main() { unsafe { let mut s = std::mem::MaybeUninit::<Stack>::uninit(); stack_init(s.as_mut_ptr()); let mut s = s.assume_init(); stack_push(&mut s, 10); stack_push(&mut s, 20); stack_push(&mut s, 30); println!("size = {}", stack_size(&s)); let mut val: c_int = 0; while stack_pop(&mut s, &mut val) { println!("popped: {}", val); } } }
cargo run
Output:
size = 3
popped: 30
popped: 20
popped: 10
Caution: When defining
#[repr(C)]structs in Rust to match C structs, you must get the field order, types, and sizes exactly right. A mismatch causes silent memory corruption. Usebindgento generate these automatically for anything non-trivial.
ABI Compatibility
ABI (Application Binary Interface) defines how functions pass arguments, return values, and lay out structs at the machine level. On x86-64 Linux, the System V AMD64 ABI passes the first six integer arguments in registers RDI, RSI, RDX, RCX, R8, R9. Return values go in RAX.
+--------+--------+--------+--------+--------+--------+
| Arg 1 | Arg 2 | Arg 3 | Arg 4 | Arg 5 | Arg 6 |
| RDI | RSI | RDX | RCX | R8 | R9 |
+--------+--------+--------+--------+--------+--------+
| Remaining args go on the stack, right to left |
+------------------------------------------------------+
When Rust uses extern "C", it follows this exact convention.
Rust Note: Rust's native ABI is not stable and can change between compiler versions. Always use
extern "C"when crossing language boundaries. The#[no_mangle]attribute prevents Rust from mangling the symbol name, making it findable by C code.
Knowledge Check
-
What is the difference between
ar rcs libfoo.a foo.oandgcc -shared -o libfoo.so foo.o? -
An executable built against
libvec2.so.1fails to run after you update the library. What might have changed? -
Why must you compile with
-fPICbefore creating a shared library?
Common Pitfalls
-
Forgetting
-fPIC. Without position-independent code, the shared library cannot be loaded at arbitrary addresses. The linker will error. -
Library search order confusion. The linker prefers
.soover.awhen both exist. Use-staticor pass the.apath directly to force static linking. -
Missing transitive dependencies. If
libA.sodepends onlibB.so, you may need to link both explicitly. Usepkg-configor CMake'starget_link_librariesto manage this. -
Forgetting
-ldlfor dlopen. On glibc systems,dlopenanddlsymlive inlibdl. Link with-ldl. -
ABI mismatch between C and Rust structs. If you define a struct in both languages, the layout must match exactly. Use
#[repr(C)]in Rust and verify withoffsetof/std::mem::offset_of!. -
Stripping symbols from a shared library. Stripping all symbols from a
.somakes it useless. Usestrip --strip-unneededto keep only the dynamic symbols.