Rust libraries¶
abicheck compares a Rust library the same way it compares a C library: as a
native shared object, with its C-visible surface read from the binary and,
optionally, from headers. Rust does not have a stable ABI. The only part of
a Rust library that is a binary contract is the part you deliberately expose as
a C ABI: #[unsafe(no_mangle)] pub extern "C" fn (#[no_mangle] in edition 2021), #[repr(C)] types, and exported
statics. That is the surface abicheck checks for Rust.
This page declares what is supported today, what is verified, and what still
behaves differently. The verdicts below come from compiling small cdylibs with
cargo and running abicheck compare on pairs of them.
Support statement¶
| Input | Status | What you get |
|---|---|---|
cdylib (.so) exporting a C ABI, compared with a C header |
Supported | Full verdicts for function, return-type, parameter, #[repr(C)] layout, exported-static, and removal changes. Works on a default cargo build --release. |
cdylib compared DWARF-only (debug = 2 in the release profile, no header) |
Supported with caveats | Same verdicts as the header path for the C surface, plus callback signatures, which the header path cannot see. See caveats. |
cdylib with no header and no DWARF (default release profile, no header) |
Not a usable check | The result is NO_CHANGE and exits 0 even when a return type changed. Do not rely on it. See caveats. |
Rust-native API (pub fn, generics, repr(Rust) types, without extern "C") |
Not supported | rustc does not export these from a cdylib, and Rust has no stable ABI for them. abicheck does not see them. |
rlib and staticlib (.rlib, .a) |
Not supported | Rejected with an explicit message (exit 64). Compare the shared library built from them instead. |
Rust generic types in a C signature (Option<extern "C" fn(...)> is the only one tested) |
Not evaluated beyond that case | The Option callback case works on the DWARF path. Other generic types have not been run. Treat their results as a review aid, not a gate. |
The policy profile rust_c_ffi (Policy Profiles) is a classification
profile for the C surface. It keeps the C-relevant checks strict and demotes the
C++ object-model kinds that cannot occur on a repr(C) / extern "C" boundary.
It does not add a Rust analyzer. None of the cases below produced an overridden
kind, so in these experiments it did not change any verdict.
Recommended setup¶
Compare the same shared object you ship. Generate the header from the Rust source with cbindgen, and keep DWARF on for the artifact you compare:
# Cargo.toml of the library you compare
[lib]
crate-type = ["cdylib"]
[profile.release]
debug = 2 # full DWARF: needed for callback signatures and for the DWARF-only path
cargo build --release
cbindgen --lang c --output include/mylib.h # regenerate per version
abicheck compare \
old/libmylib.so new/libmylib.so \
-H old=old/include/mylib.h -H new=new/include/mylib.h \
--policy rust_c_ffi
debug = 2 adds debug information only; it does not change the code that is
generated. Keep the crate name stable across versions (see
caveats).
What was verified¶
Each row is one comparison of a baseline cdylib against a variant with one change. Headers are from cbindgen unless marked otherwise.
| Change | Header, default release (no DWARF) | DWARF only (debug = 2) |
No header, no DWARF (default release) |
|---|---|---|---|
| Exported function added | COMPATIBLE |
COMPATIBLE_WITH_RISK (see caveats) |
COMPATIBLE_WITH_RISK |
Exported function return type changed (i32 → i64) |
BREAKING (func_return_changed) |
BREAKING (func_return_changed) |
NO_CHANGE, exit 0 (missed) |
#[repr(C)] struct gained a field |
BREAKING (type_size_changed) |
BREAKING |
NO_CHANGE, exit 0 (missed) |
#[repr(C)] union gained a member |
BREAKING (type_size_changed, union_field_added) |
BREAKING |
not run |
#[repr(C)] enum value changed |
BREAKING (enum_member_value_changed) |
BREAKING |
not run |
#[repr(C)] enum member added |
COMPATIBLE (enum_member_added) |
COMPATIBLE |
not run |
Field array size changed ([u8; 16] → [u8; 32]) |
BREAKING |
BREAKING (size and offset; the array type change is not reported) |
not run |
Exported static retyped (i32 → i64) |
BREAKING (var_type_changed) |
BREAKING |
BREAKING (symbol_size_changed only) |
| Exported function removed | BREAKING (func_removed) |
BREAKING |
BREAKING (func_removed_elf_only) |
Callback parameter arity changed (extern "C" fn(i32) → fn(i32, i32)) |
NO_CHANGE (missed) |
BREAKING (func_params_changed) |
not run |
pub fn (not exported) changed signature |
NO_CHANGE |
NO_CHANGE with a crate name held fixed |
NO_CHANGE |
.rlib / .a input |
rejected, exit 64 | rejected, exit 64 | rejected, exit 64 |
The header rows use the header the variant's own cbindgen run produced. The "Callback parameter arity" row is the one place the header path is weaker: see caveats.
Caveats¶
These are the behaviors a Rust maintainer should know. Each has a tracked entry in Known gaps.
- A release build without debug info is not a check on its own. With the
Cargo default (
debug = 0), abicheck has no type information unless you pass headers. Without headers, a real return-type break readsNO_CHANGE, and the report saysanalysis_assurance: partial. Always pass cbindgen headers, or build withdebug = 2. debug = 1is not enough.debug = 1emits DWARF but leaves out the function types. The return-type break above was missed withanalysis_assurance: complete. Usedebug = 2.- Callback signatures are invisible to the header path. A typedef'd
extern "C"function pointer changes arity under the same name; abicheck reads the parameter as "a pointer" and reports no change. The DWARF path sees it. This is not Rust-specific; C function pointers behave the same way. - Renaming the crate changes every type's identity. DWARF names are
crate-qualified (
mylib::Point). Renaming the crate with an unchanged C layout reportstype_removedandtype_added, which givesAPI_BREAKon the DWARF path. The header path reportsNO_CHANGE. Keep the crate name stable, or compare with headers. - Header-less additive changes can carry a risk finding. Without headers,
an added exported function can be followed by
exported_object_alignment_reducedon an unrelated exported static. That alignment is derived from the symbol's link-time address, not its declared alignment. With headers the finding does not appear. - Rust generics leave
Option<...>-style pairs. A type such asOption<extern "C" fn(i32) -> i32>embeds its signature in its name, so a signature change shows as atype_removed/type_addedpair next to the realfunc_params_changed. The arity finding that used to accompany this was a bug and is fixed.
Fixed for Rust¶
Two problems surfaced during this check and were fixed in the same change:
- A static archive (
.a,.rlib) was sniffed as a Debian package because both start with the!<arch>magic, and failed with "No data.tar.* found in Deb package". A.debis now recognised by its first member,debian-binary, so archives get the explicit "static/import library archive" rejection. - A function-typed template argument (
Option<extern "C" fn(i32, i32) -> i32>) was counted as having two template arguments, because the commas of its parameter list and the>of its->return arrow were read as template syntax. That produced a falsemandatory_template_param_added. The counter now ignores both.
Tests¶
tests/test_rust_cdylib_integration.py(markerintegration, skipped withoutcargo) builds real cdylibs and checks the header path, the DWARF path, the callback arity case, and the non-exported Rust-native case.tests/test_deb_extractor_detection.pyandtests/test_diff_templates_fn_type_arity.pycover the two fixes without a toolchain.
See also¶
- Policy Profiles: the
rust_c_ffiprofile and the other built-ins. - Platform Support: what is analysed on which host.
- Known gaps: the open entries behind the caveats.