cdylib and staticlib crates
A library crate is a .rlib that only Rust can link. The same source can produce a shared library a C program loads at run time, or a static library the C linker copies into the executable, and the difference is one line in the manifest.
This lesson builds one function into both kinds of library, calls it from a C program with gcc, and ends with the linker error for a symbol the library does not contain. The boundary itself is the subject of the lesson on calling C.
A shared library is also how a Python script calls Rust, with or without a binding crate: Extending Python with Rust builds both kinds of boundary.
A shared library
crate-type = ["cdylib"] asks for a shared object with no Rust metadata: libfastmath.so on Linux, .dylib on macOS and .dll on Windows. The exported function needs both attributes, the C calling convention and the kept symbol name.
#[unsafe(no_mangle)]
pub extern "C" fn fast_double(value: i32) -> i32 {
value * 2
}
[lib]
crate-type = ["cdylib"]
cargo build --release
ls target/release/*.so
vi main.c
gcc main.c -L target/release -lfastmath -o use_shared
LD_LIBRARY_PATH=target/release ./use_shared
#include <stdio.h>
int fast_double(int value);
int main(void)
{
printf("%d\n", fast_double(21));
return 0;
}
Compiling fastmath v0.1.0 (/tmp/rw7/L086/fastmath)
Finished `release` profile [optimized] target(s) in 0.63s
target/release/libfastmath.so
42
The C program declares the function by hand, because the library ships no header, and the linker finds it through -lfastmath plus the folder given to -L. At run time the loader needs the same folder in LD_LIBRARY_PATH, or the program stops before main with a message about the missing library.
An argument that is a struct needs #[repr(C)] on the Rust side and the same declaration on the C side, because the two compilers must agree on the offsets before the call can work. A pointer to an opaque type is simpler: C keeps a void * and the Rust side keeps the layout, which is how a library hides its internals from the caller.
A static library
crate-type = ["staticlib"] produces libfastmath.a, an archive the C linker copies into the executable. The result needs no library at run time, and it is the form a plugin or an embedded target usually wants.
The same source file and the same C program are used, and only the linking command changes: the archive is named on the command line instead of a -l flag.
[lib]
crate-type = ["staticlib"]
cargo build --release
ls target/release/*.a
gcc main.c target/release/libfastmath.a -o use_static
./use_static
Compiling fastmath v0.1.0 (/tmp/rw7/L086/fastmath)
Finished `release` profile [optimized] target(s) in 0.44s
target/release/libfastmath.a
42
The output is the same, and the difference is visible in the file: the static program is larger because the code of the library is inside it, and it starts with no lookup. A Rust static library also pulls in the parts of the standard library it uses, which is why the archive is much larger than the one function suggests.
Both crate types
The list takes more than one entry, and cargo writes both artefacts from the same compilation. A package that ships a shared library for a plugin and a static one for an embedded build keeps both in one place.
[lib]
crate-type = ["cdylib", "staticlib"]
cargo build --release
ls target/release/*.a target/release/*.so
Compiling fastmath v0.1.0 (/tmp/rw7/L086/fastmath)
Finished `release` profile [optimized] target(s) in 0.34s
target/release/libfastmath.a
target/release/libfastmath.so
Both files come from the release profile, and neither replaces the .rlib that a Rust binary of the same package would use. The previous lesson is the target list of a package that stays inside Rust.
A symbol the linker cannot find
The compiler trusts the declaration in the extern block and the linker is the first tool that checks it. A function that no library provides fails at link time, with the symbol name and the note about the -l flag.
The program declares fast_triple and calls it. The library exports only fast_double, so the link fails.
unsafe extern "C" {
fn fast_triple(value: i32) -> i32;
}
fn main() {
println!("{}", unsafe { fast_triple(14) });
}
rustc --edition 2024 missing.rs -o missing
error: linking with `cc` failed: exit status: 1
|
= note: some arguments are omitted. use `--verbose` to show all linker arguments
= note: /usr/lib/gcc/x86_64-alpine-linux-musl/15.2.0/../../../../x86_64-alpine-linux-musl/bin/ld: missing.missing.d75f493b5e8b340c-cgu.0.rcgu.o: in function `missing::main':
missing.d75f493b5e8b340c-cgu.0:(.text._ZN7missing4main17h37ccd10ffe7d4aebE+0xb): undefined reference to `fast_triple'
collect2: error: ld returned 1 exit status
= note: some `extern` functions couldn't be found; some native libraries may need to be installed or have their path specified
= note: use the `-l` flag to specify native libraries to link
error: aborting due to 1 previous error
The middle line of the output is the linker command, which names every object and library it was given, and it is omitted here because it changes with the target folder. The three notes are the useful part: the symbol, the possibility of a missing native library and the flag that adds one. The next lesson moves a crate between editions, which is the last change a Rust project usually makes.
Exercises
- Export a function that fills a caller-provided buffer and call it from C.
- Build the shared library and run the C program without LD_LIBRARY_PATH; read the loader message.
- Compare the sizes of the static and the shared artefact with ls -l and explain the difference.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which manifest entry produces a shared library?
Answer
crate-type = ["cdylib"] under [lib]
The crate type tells cargo which artefact to write, and cdylib is the C-compatible shared one.
-
What is the difference between a cdylib and a staticlib at run time?
Answer
The static library is copied into the program, the shared one is loaded
A static archive is linked in, and a shared object stays a separate file.
-
Why does an exported function need
extern "C"?Answer
It gives the function the calling convention C expects
The convention decides how arguments and the return value travel between the two languages.
-
Which tool reports a symbol that no library provides?
Answer
The linker
The declaration compiles and only the link step knows which symbols exist.
-
What does this program print?
#[unsafe(no_mangle)] pub extern "C" fn fast_double(value: i32) -> i32 { value * 2 } fn main() { println!("{}", fast_double(21)); }Answer
42
The attributes change the symbol and the convention, and the call inside Rust stays ordinary.
-
What does this program print?
fn main() { let value = 21; let doubled = value * 2; println!("{} {}", doubled, value); }Answer
42 21
The multiplication does not consume the integer, which is Copy.
-
What does this program print?
use std::mem::size_of; fn main() { println!("{}", size_of::<extern "C" fn(i32) -> i32>()); }Answer
8
A function pointer is one machine word, whatever the convention.
-
What does this program print?
#[unsafe(no_mangle)] pub extern "C" fn add(left: i32, right: i32) -> i32 { left + right } fn main() { println!("{}", add(40, 2)); }Answer
42
The attribute only matters when the symbol leaves the crate, and the call is a normal one.
-
What is the first line of the compiler output for this program?
unsafe extern "C" { fn fast_triple(value: i32) -> i32; } fn main() { println!("{}", unsafe { fast_triple(14) }); }Answer
error: linking with
ccfailed: exit status: 1The declaration compiles, and the missing symbol is reported by the linker.
-
What is the first line of the compiler output for this program?
#[unsafe(no_mangle)] pub extern "C" fn fast_double(value: i32) -> i32 { value * 2 } fn main() { println!("{}", fast_double("x")); }Answer
error[E0308]: mismatched types
The parameter is an integer and the call passes a string, which the type checker rejects.
The code on this page was built with rustc 1.96.1 and cargo 1.96.1, and the C programs with gcc 15.2.0; all of them were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev