Rust staticlib versus cdylib: Deliver the Right Artifact to a C Application
Choose staticlib when the C application’s final link should absorb the Rust implementation. Choose cdylib when the implementation should remain a separately loaded system library. Neither choice removes the need for a deliberate C ABI, native dependency closure and a consumer-side smoke test.
This guide uses Rust 1.85.0 documentation and an explicitly selected Rust 2021 edition for its example. The commands are proposed fixtures: no verified Rust toolchain was available, so neither artifact was built or executed here. Linux ELF inspection is described separately from Rust’s cross-platform artifact rules.
Choose the artifact by its consumer
The versioned linkage reference distinguishes Rust library artifacts from system libraries. An rlib contains information intended for later rustc use. A Rust dylib serves Rust linkage. Handing either to a C integrator as though it were the documented native integration artifact leaves important assumptions unstated.
| Artifact | C integration consequence |
|---|---|
| staticlib | System static library containing the crate and upstream Rust dependencies; the C final link still resolves required native dependencies. |
| cdylib | System dynamic library intended for a consumer in another language; packaging must satisfy its runtime library dependencies. |
| rlib or Rust dylib | Rust linkage products, not substitutes for a deliberately specified foreign-language handoff. |
A staticlib can include Rust’s standard library without becoming a fully self-contained executable. Native dynamic libraries cannot simply be embedded into the archive. The C application’s final link and the resulting executable’s runtime requirements remain part of the integration contract.

Make the C boundary small and explicit
Use a narrow exported interface with specified integer widths, calling convention and ownership. For the proposed smoke test, the Rust source is deliberately independent of allocation, pointers and callbacks:
// bridge.rs; Rust 1.85.0, edition 2021
#[no_mangle]
pub extern "C" fn bridge_add(a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}
The corresponding C declaration is uint32_t bridge_add(uint32_t, uint32_t), using stdint.h. The Rust function reference documents the extern ABI boundary. The explicit edition matters here: this example is not an unqualified template for Rust 2024 attribute syntax.
A real interface needs more than fixed-width arguments. Specify who allocates and frees returned memory, whether pointers may be null, whether callbacks can reenter and how errors are represented. Do not export Rust String, Vec or an unspecified Rust-layout structure as a casual C contract.
Keep panics from becoming an undocumented foreign-call behavior. An aborting panic policy terminates the process; it does not translate a failure into a C error return. This arithmetic fixture avoids a panic path, but a production interface still needs an explicit error and termination policy.
Build each candidate in a clean fixture directory
For a native Linux qualification with the pinned compiler, the proposed producer commands are:
rustc --edition=2021 --crate-type=staticlib bridge.rs -o libbridge.a --print=native-static-libs
rustc --edition=2021 --crate-type=cdylib bridge.rs -o libbridge.so
Save the staticlib diagnostic and actual producer command. The rustc command-line reference documents native-static-libs as a way to obtain native linker flags for a staticlib. Do not copy a dependency list from a different target, compiler release or feature configuration.
For cross compilation, select the Rust target and its qualified native linker/toolchain for both builds. Then use the matching target C compiler for the consumer. A host smoke link cannot establish target libc compatibility, target instruction availability or that the embedded loader exists.
Make the C application prove closure
The proposed consumer should return failure unless bridge_add(20, 22) equals 42 and bridge_add(UINT32_MAX, 1) equals zero. The second check makes the example’s wrapping behavior part of the contract rather than an incidental implementation detail.
/* consumer.c: proposed C smoke test */
#include <stdint.h>
extern uint32_t bridge_add(uint32_t, uint32_t);
int main(void) {
return bridge_add(20, 22) != 42 ||
bridge_add(UINT32_MAX, 1) != 0;
}
Compile that same consumer twice. For static integration, name libbridge.a explicitly and append the native libraries reported for that build in the required link order. For dynamic integration, link the fixture libbridge.so and execute with a controlled loader path confined to the fixture directory.
Inspect each resulting ELF file. A staticlib consumer should not unexpectedly depend on libbridge.so, but it may still require libc or other native shared objects. A cdylib consumer should have the intended library relationship, and the shared object itself may introduce further runtime dependencies.
Read exported dynamic symbols from the cdylib and unresolved references from the archive as separate views. Undefined archive symbols are not automatically defects: many are resolved by other archive members or the final native link. The decisive test is the complete consumer closure, followed by execution on the intended platform.
Preserve export and deployment policy
The linkage reference warns that a staticlib includes public symbols from upstream dependencies. If an integrator folds that archive into another shared library, review the final DSO’s exported symbol set rather than assuming the Rust wrapper is its only visible surface.
For a cdylib, define the deployed filename and library identity policy, ownership of upgrades and the supported C interface revision. The crate type alone does not design a compatible upgrade scheme. Package the required native runtime libraries and test from the final image layout.
Make the handoff reproducible
Deliver the chosen artifact together with a C header, Rust toolchain and target identity, feature configuration, native dependency list and exact consumer fixture. Preserve both positive calls and a link failure control such as deliberately omitting a required native dependency when one exists.
Choose staticlib for final-link integration ownership; choose cdylib when the separately deployed library is the intended unit. Neither is universally smaller, faster or easier to update. Those outcomes depend on the final image and workload. The acceptance criterion is a documented native boundary whose dependencies and consumer behavior were actually qualified.