no mangle export
Tutorial
The Problem
This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming — writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.
🎯 Learning Outcomes
Code Example
use std::os::raw::c_int;
/// Add two integers — symbol emitted as `rust_add` with C ABI.
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
/// Fibonacci, iterative. Returns -1 for negative input (C-style error code).
#[no_mangle]
pub extern "C" fn rust_fib(n: c_int) -> c_int {
if n < 0 { return -1; }
if n <= 1 { return n; }
let (mut a, mut b) = (0i32, 1i32);
for _ in 2..=n {
let c = a.wrapping_add(b);
a = b; b = c;
}
b
}Key Differences
unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.extern "C"; OCaml uses ctypes which wraps C types in OCaml values.#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.OCaml Approach
OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:
ctypes library for external function callsBigarray for controlled raw memory access Bytes.t for mutable byte sequencesOCaml programs rarely need operations equivalent to these Rust unsafe patterns.
Full Source
#![allow(clippy::all)]
//! 711 — `#[no_mangle]` Exporting Rust Functions to C
//!
//! `#[no_mangle] pub extern "C" fn` is the declaration that turns a Rust
//! function into a stable, C-ABI symbol any foreign language can call.
//!
//! Without `#[no_mangle]`, the Rust compiler encodes the full module path,
//! generic parameters, and a hash into the symbol name (name mangling), making
//! it impossible for C to find the function by a known name.
//!
//! The ABI contract at the boundary: no Rust-only types (`String`, `Vec`,
//! `Result`), no panics, only C-compatible scalars and raw pointers.
use std::os::raw::{c_char, c_int};
// ── Exported symbols ──────────────────────────────────────────────────────────
/// Add two C integers. Exported as the bare symbol `rust_add`.
///
/// The `extern "C"` qualifier switches from Rust's default calling convention
/// to the platform C ABI so that C callers can push/pop arguments as expected.
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
/// Compute the nth Fibonacci number. Returns -1 for negative input.
///
/// Uses an iterative accumulator — a direct loop is cleaner than an iterator
/// chain here because we need two mutable bindings updated in lockstep.
#[no_mangle]
pub extern "C" fn rust_fib(n: c_int) -> c_int {
if n < 0 {
return -1;
}
if n <= 1 {
return n;
}
let (mut a, mut b) = (0i32, 1i32);
for _ in 2..=n {
let c = a.wrapping_add(b);
a = b;
b = c;
}
b
}
/// Absolute value of a C integer.
#[no_mangle]
pub extern "C" fn rust_abs(n: c_int) -> c_int {
n.abs()
}
/// Return a pointer to a static, null-terminated version string.
///
/// SAFETY for the C caller: the pointer is valid for the lifetime of the
/// process (it points to a `'static` byte literal), is null-terminated, and
/// must not be mutated or freed. These invariants are documented in the
/// accompanying C header.
#[no_mangle]
pub extern "C" fn rust_version() -> *const c_char {
// `c"1.0.0"` is a C string literal (Rust 1.77+): null-terminated, stored
// in `.rodata`, zero allocation. `.as_ptr()` yields `*const c_char`.
c"1.0.0".as_ptr()
}
/// Clamp `value` to the inclusive range `[lo, hi]`.
/// Returns `lo` when `value < lo`, `hi` when `value > hi`.
/// If `lo > hi` the behaviour is unspecified (mirrors C's convention of
/// leaving invalid ranges to the caller).
#[no_mangle]
pub extern "C" fn rust_clamp(value: c_int, lo: c_int, hi: c_int) -> c_int {
value.clamp(lo, hi)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
// We call the `#[no_mangle]` functions directly from Rust — the C ABI is
// transparent to the Rust test runner, which links and calls them normally.
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
#[test]
fn test_add_basic() {
assert_eq!(rust_add(3, 4), 7);
assert_eq!(rust_add(0, 0), 0);
assert_eq!(rust_add(-5, 5), 0);
assert_eq!(rust_add(-3, -4), -7);
}
#[test]
fn test_fib_sequence() {
// F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(10)=55
assert_eq!(rust_fib(0), 0);
assert_eq!(rust_fib(1), 1);
assert_eq!(rust_fib(2), 1);
assert_eq!(rust_fib(5), 5);
assert_eq!(rust_fib(10), 55);
}
#[test]
fn test_fib_negative_returns_sentinel() {
assert_eq!(rust_fib(-1), -1);
assert_eq!(rust_fib(-100), -1);
}
#[test]
fn test_abs() {
assert_eq!(rust_abs(0), 0);
assert_eq!(rust_abs(42), 42);
assert_eq!(rust_abs(-42), 42);
assert_eq!(rust_abs(i32::MAX), i32::MAX);
}
#[test]
fn test_version_is_valid_c_string() {
let ptr = rust_version();
assert!(!ptr.is_null());
// SAFETY: rust_version() returns a pointer to a 'static null-terminated
// byte literal. It is valid, non-null, and properly terminated.
let s = unsafe { CStr::from_ptr(ptr) };
assert_eq!(s.to_str().unwrap(), "1.0.0");
}
#[test]
fn test_clamp_within_and_out_of_range() {
assert_eq!(rust_clamp(5, 0, 10), 5);
assert_eq!(rust_clamp(-1, 0, 10), 0);
assert_eq!(rust_clamp(15, 0, 10), 10);
assert_eq!(rust_clamp(0, 0, 0), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CStr;
#[test]
fn test_add_basic() {
assert_eq!(rust_add(3, 4), 7);
assert_eq!(rust_add(0, 0), 0);
assert_eq!(rust_add(-5, 5), 0);
assert_eq!(rust_add(-3, -4), -7);
}
#[test]
fn test_fib_sequence() {
// F(0)=0, F(1)=1, F(2)=1, F(3)=2, F(4)=3, F(5)=5, F(10)=55
assert_eq!(rust_fib(0), 0);
assert_eq!(rust_fib(1), 1);
assert_eq!(rust_fib(2), 1);
assert_eq!(rust_fib(5), 5);
assert_eq!(rust_fib(10), 55);
}
#[test]
fn test_fib_negative_returns_sentinel() {
assert_eq!(rust_fib(-1), -1);
assert_eq!(rust_fib(-100), -1);
}
#[test]
fn test_abs() {
assert_eq!(rust_abs(0), 0);
assert_eq!(rust_abs(42), 42);
assert_eq!(rust_abs(-42), 42);
assert_eq!(rust_abs(i32::MAX), i32::MAX);
}
#[test]
fn test_version_is_valid_c_string() {
let ptr = rust_version();
assert!(!ptr.is_null());
// SAFETY: rust_version() returns a pointer to a 'static null-terminated
// byte literal. It is valid, non-null, and properly terminated.
let s = unsafe { CStr::from_ptr(ptr) };
assert_eq!(s.to_str().unwrap(), "1.0.0");
}
#[test]
fn test_clamp_within_and_out_of_range() {
assert_eq!(rust_clamp(5, 0, 10), 5);
assert_eq!(rust_clamp(-1, 0, 10), 0);
assert_eq!(rust_clamp(15, 0, 10), 10);
assert_eq!(rust_clamp(0, 0, 0), 0);
}
}
Deep Comparison
OCaml vs Rust: #[no_mangle] Exporting Functions to C
Side-by-Side Code
OCaml
(* OCaml uses Callback.register to make functions callable from C.
C code calls caml_named_value("rust_add") to get a closure handle,
then invokes it with caml_callback2(..., Val_int(a), Val_int(b)).
The GC, boxing, and tagging remain hidden from the C caller. *)
let () =
Callback.register "rust_add" (fun a b -> (a : int) + b);
Callback.register "rust_fib" (fun n ->
let rec fib k = if k <= 1 then k else fib (k-1) + fib (k-2) in
fib (n : int)
)
Rust (idiomatic export)
use std::os::raw::c_int;
/// Add two integers — symbol emitted as `rust_add` with C ABI.
#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
a + b
}
/// Fibonacci, iterative. Returns -1 for negative input (C-style error code).
#[no_mangle]
pub extern "C" fn rust_fib(n: c_int) -> c_int {
if n < 0 { return -1; }
if n <= 1 { return n; }
let (mut a, mut b) = (0i32, 1i32);
for _ in 2..=n {
let c = a.wrapping_add(b);
a = b; b = c;
}
b
}
Rust (static string export)
use std::os::raw::c_char;
/// Return a pointer to a 'static, null-terminated version string.
#[no_mangle]
pub extern "C" fn rust_version() -> *const c_char {
b"1.0.0\0".as_ptr().cast()
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Export mechanism | Callback.register "name" fn | #[no_mangle] pub extern "C" fn name(...) |
| Integer type | int (tagged boxed OCaml int) | c_int = i32 on most platforms |
| C string | Not natively supported; use Bytes + stubs | *const c_char pointing to a b"...\0" literal |
| Error signalling | option / result (OCaml-only) | Return code convention (-1, NULL) |
| Symbol visibility | Named via runtime registry | Compile-time symbol in the object file |
Key Insights
#[no_mangle] suppresses this, making the emitted symbol byte-for-byte what the source says — rust_add → rust_add. OCaml never mangles in the same sense; instead it uses a runtime name registry (Callback.register) that C must navigate through the OCaml runtime API.pub extern "C" switches from Rust's default (unspecified) calling convention to the platform C ABI: arguments on the stack or in registers in the C-defined order, return in the C-defined register. Without extern "C", the binary interface is undefined and C cannot reliably call the function even if the symbol is visible.Callback mechanism still passes OCaml-boxed values (Val_int, caml_callback2); the C caller must understand the OCaml representation. Rust's FFI exports use only C primitives (c_int, *const c_char) — the C caller needs nothing Rust-specific at all. The discipline cost: no Result, no String, no panics — errors become return codes.b"1.0.0\0".as_ptr().cast::<c_char>() returns a pointer into the .rodata segment — zero heap cost, valid for the process lifetime. The C caller gets a const char * that needs no free. OCaml would require Bytes marshalling or a C stub with caml_copy_string.#[no_mangle] pub extern "C" functions are still ordinary Rust functions at the type level; the Rust test runner calls them directly without going through the C ABI. This means you get full cargo test coverage on your exports with no extra test harness.When to Use Each Style
**Use #[no_mangle] pub extern "C" when:** building a shared library (cdylib) consumed by C, Python (ctypes/cffi), Node.js (ffi-napi), or any other language that can load native symbols. This is the standard "Rust as a library" pattern.
**Use OCaml Callback.register when:** embedding OCaml inside a C host application and needing OCaml closures callable from C; the C side must link against the OCaml runtime and use caml_callback helpers rather than calling the function directly.
Exercises
bytemuck for transmute, CString for FFI strings) and implement it.