Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust FAQ: Linkage to/Relationship with C

PART21 -- Linkage to/Relationship with C

Q106: How can I call a C function from Rust code?

To call a C function from Rust, you use Rust’s Foreign Function Interface (FFI), which allows safe interaction with C code. This involves declaring the C function in Rust with the extern "C" block and linking to the C library. Here’s how:

Steps:

  1. Declare the C Function:
    • Use an extern "C" block to define the C function’s signature.
    • Specify the function’s name, arguments, and return type, matching the C declaration exactly.
    • Use Rust types that correspond to C types (e.g., c_int for int, c_void for void).
  2. Link to the C Library:
    • Use the #[link] attribute or build tools to link the C library.
  3. Call the Function:
    • Use unsafe to call the C function, as Rust cannot guarantee its safety.

Example: Calling the C sqrt function from the standard math library (libm).

use std::os::raw::c_double;

// Declare the C function
extern "C" {
    fn sqrt(x: c_double) -> c_double;
}

fn main() {
    let x = 16.0;
    let result = unsafe { sqrt(x) }; // Unsafe call to C function
    println!("Square root of {} is {}", x, result); // Prints: Square root of 16 is 4
}

Cargo Configuration (for linking libm): In Cargo.toml:

[package]
name = "my_crate"
version = "0.1.0"
edition = "2024"

[dependencies]

[build-dependencies]
cc = "1.0"

Create a build.rs to link the library:

fn main() {
    println!("cargo:rustc-link-lib=m"); // Links libm
}

Key Points:

  • Type Mapping: Use std::os::raw types (e.g., c_int, c_char, c_void) or libc crate for standard C types.
  • Unsafe: C functions are called in an unsafe block because Rust cannot verify their memory safety.
  • Linking: Specify the library with #[link(name = "m")] or via build.rs.
  • Header Files: For complex C libraries, use tools like bindgen to generate Rust bindings from C headers.
    • Example: Add bindgen to build-dependencies in Cargo.toml, then use it to generate bindings:
      // build.rs
      fn main() {
          bindgen::Builder::default()
              .header("wrapper.h") // Wraps C header
              .generate()
              .expect("Unable to generate bindings")
              .write_to_file("src/bindings.rs")
              .expect("Couldn't write bindings!");
      }

Best Practice:

  • Use the libc crate for standard C types and functions.
  • Use bindgen for automatic binding generation from C headers.
  • Minimize unsafe code by wrapping C calls in safe Rust abstractions.
  • Test thoroughly, as C functions may introduce undefined behavior.

Q107: How can I create a Rust function callable from C code?

To create a Rust function callable from C, you need to expose the function with the C calling convention using extern "C" and ensure it’s compatible with C’s type system. Here’s how:

Steps:

  1. Define the Function:
    • Use extern "C" to specify the C calling convention.
    • Use #[no_mangle] to prevent Rust from mangling the function name (C expects unmangled names).
    • Use C-compatible types (e.g., i32 for int, *const c_char for const char*).
  2. Ensure Safety:
    • Avoid Rust-specific types (e.g., String, Vec) in the function signature; use raw pointers or C types.
    • Handle memory manually if passing pointers.
  3. Compile as a Library:
    • Configure your crate as a cdylib or staticlib in Cargo.toml to produce a C-compatible library.

Example: A Rust function that adds two integers, callable from C.

#![allow(unused)]
fn main() {
use std::os::raw::c_int;

#[no_mangle]
pub extern "C" fn rust_add(a: c_int, b: c_int) -> c_int {
    a + b
}
}

Cargo.toml:

[package]
name = "my_crate"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"] # Dynamic library for C

C Code to Call It:

#include <stdint.h>

extern int32_t rust_add(int32_t a, int32_t b);

int main() {
    int32_t result = rust_add(5, 3);
    printf("Result: %d\n", result); // Prints: Result: 8
    return 0;
}

Compile and Link:

  1. Build the Rust library: cargo build --release.
  2. Link the C code with the generated library (e.g., libmy_crate.so or libmy_crate.a):
    gcc main.c -L target/release -lmy_crate -o my_program
    

Key Points:

  • C-Compatible Types: Use std::os::raw types (e.g., c_int, c_char) or libc equivalents.
  • No Mangle: #[no_mangle] ensures the function name is rust_add instead of a mangled Rust name.
  • Safety: Rust guarantees the function’s safety internally, but C callers must respect memory contracts (e.g., not passing null pointers unless specified).
  • Library Type: Use cdylib for dynamic linking or staticlib for static linking, depending on your needs.

Best Practice:

  • Use simple, C-compatible types in function signatures.
  • Document memory ownership (e.g., who frees pointers) in comments or headers.
  • Test the C-Rust interface thoroughly to catch ABI mismatches.
  • Use bindgen or cbindgen to generate C header files from Rust code:
    • Add cbindgen to build-dependencies, then run cbindgen --output my_crate.h.

Q108: Why do I get linker errors when calling C/Rust functions cross-language?

Linker errors when calling C functions from Rust or Rust functions from C typically arise from mismatches in the Application Binary Interface (ABI), missing libraries, incorrect function declarations, or improper build configuration. Here are common causes and solutions:

  • Common Causes:

    1. Missing Library:
      • The C library (e.g., libm) isn’t linked, causing “undefined symbol” errors.
      • Solution: Add the library in build.rs or with #[link]:
        // build.rs
        fn main() {
            println!("cargo:rustc-link-lib=m"); // Links libm
        }
    2. Name Mangling:
      • Rust mangles function names unless #[no_mangle] is used, making them invisible to C.
      • Solution: Add #[no_mangle] to Rust functions callable from C:
        #![allow(unused)]
        fn main() {
        #[no_mangle]
        pub extern "C" fn my_function() {}
        }
    3. ABI Mismatch:
      • The function signature in Rust doesn’t match the C declaration (e.g., wrong types or calling convention).
      • Solution: Ensure signatures match exactly, using C-compatible types:
        #![allow(unused)]
        fn main() {
        extern "C" {
            fn c_function(x: i32); // Must match C: void c_function(int x);
        }
        }
    4. Library Path Issues:
      • The linker can’t find the C library or Rust cdylib/staticlib.
      • Solution: Specify library paths in the build process:
        gcc main.c -L target/release -lmy_crate
        
      • Or use build.rs to set paths:
        #![allow(unused)]
        fn main() {
        println!("cargo:rustc-link-search=native=/path/to/lib");
        }
    5. Platform-Specific Issues:
      • Different platforms (e.g., Windows vs. Linux) have different linking conventions or library names.
      • Solution: Use conditional compilation or platform-specific build scripts:
        #![allow(unused)]
        fn main() {
        #[cfg(target_os = "linux")]
        println!("cargo:rustc-link-lib=m");
        }
  • Example Error:

    undefined reference to `sqrt`
    

    Fix: Link the math library (libm):

    // build.rs
    fn main() {
        println!("cargo:rustc-link-lib=m");
    }
  • Debugging Tips:

    • Check function signatures in both Rust and C for exact matches.
    • Use nm or objdump to inspect symbols in the compiled library:
      nm -g target/release/libmy_crate.a
      
    • Ensure extern "C" is used for C calling conventions.
    • Verify library paths and names in build scripts or linker commands.
    • Use bindgen (for C-to-Rust) or cbindgen (for Rust-to-C) to generate correct bindings.

Best Practice:

  • Use build.rs for reliable linking.
  • Test cross-language calls with small examples before scaling up.
  • Document ABI requirements (e.g., pointer ownership) clearly.
  • Use tools like bindgen and cbindgen to minimize manual errors.

Q109: How can I pass a Rust struct to/from a C function?

To pass a Rust struct to or from a C function, you need to ensure the struct is C-compatible and follows the C ABI. This involves using #[repr(C)] to guarantee a C-compatible memory layout and handling ownership carefully.

Steps:

  1. Define a C-Compatible Struct:
    • Use #[repr(C)] to ensure the struct’s fields are laid out in memory as C expects (no Rust-specific reordering or padding).
    • Use C-compatible types (e.g., i32 for int, *const c_char for char*).
  2. Pass to C:
    • Pass the struct as a pointer (*const MyStruct or *mut MyStruct) to avoid copying and respect C’s conventions.
    • Use unsafe for C calls.
  3. Receive from C:
    • Accept a pointer to the struct and convert it back to a Rust reference.
    • Ensure proper lifetime and ownership management.
  4. Handle Ownership:
    • Clearly define who owns the struct’s memory (Rust or C) to avoid leaks or double frees.

Example: Passing a Rust struct to a C function and receiving one back.

use std::os::raw::{c_int, c_char};
use std::ffi::CString;

// Rust struct with C-compatible layout
#[repr(C)]
pub struct Point {
    x: c_int,
    y: c_int,
}

// Declare C function
extern "C" {
    fn process_point(p: *const Point) -> *mut Point;
}

// Rust wrapper for safety
fn call_c_function(p: &Point) -> Point {
    unsafe {
        let result = process_point(p as *const Point);
        *result // Dereference and copy
    }
}

fn main() {
    let point = Point { x: 10, y: 20 };
    let result = call_c_function(&point);
    println!("Result: x={}, y={}", result.x, result.y);
}

C Code (example process_point):

#include <stdlib.h>

typedef struct {
    int x;
    int y;
} Point;

Point* process_point(const Point* p) {
    Point* result = (Point*)malloc(sizeof(Point));
    result->x = p->x + 1;
    result->y = p->y + 1;
    return result;
}

Key Points:

  • #[repr(C)]: Ensures the Rust struct matches C’s memory layout.
  • Pointers: Pass structs as *const Point or *mut Point to C, as C expects pointers for structs.
  • Ownership: In the example, C allocates a new Point with malloc, and Rust must free it (not shown for brevity). Use std::mem::forget or manual freeing to avoid leaks.
  • Safety: Use unsafe for C calls and pointer dereferences, and validate pointers (e.g., check for null).

Ownership Example: To free a C-allocated struct in Rust:

#![allow(unused)]
fn main() {
use std::ptr;

extern "C" {
    fn free_point(p: *mut Point);
}

fn call_c_function_safe(p: &Point) -> Point {
    unsafe {
        let result = process_point(p as *const Point);
        let value = ptr::read(result); // Copy data
        free_point(result); // Free C-allocated memory
        value
    }
}
}

Best Practice:

  • Always use #[repr(C)] for structs passed to/from C.
  • Document ownership rules (e.g., who allocates/frees memory).
  • Use bindgen to generate Rust bindings for C structs and functions.
  • Wrap FFI calls in safe Rust functions to minimize unsafe usage.

Q110: Can my C function access data in a Rust struct?

Yes, a C function can access data in a Rust struct if the struct is C-compatible (uses #[repr(C)]) and is passed as a pointer. However, you must ensure proper memory layout, type compatibility, and ownership handling to avoid undefined behavior.

Steps:

  1. Define a C-Compatible Struct:
    • Use #[repr(C)] to ensure the Rust struct’s memory layout matches C’s expectations.
    • Use C-compatible types for fields (e.g., c_int for int, *mut c_char for char*).
  2. Pass the Struct to C:
    • Pass a pointer (*const MyStruct for read-only, *mut MyStruct for mutable) to the C function.
  3. Access in C:
    • The C code can dereference the pointer to access fields, assuming the layout matches.
  4. Handle Ownership:
    • Ensure Rust and C agree on who owns the struct’s memory to avoid leaks or double frees.

Example: Rust struct passed to a C function to read its fields.

use std::os::raw::c_int;

// Rust struct
#[repr(C)]
pub struct Point {
    x: c_int,
    y: c_int,
}

extern "C" {
    fn print_point(p: *const Point);
}

fn main() {
    let point = Point { x: 10, y: 20 };
    unsafe {
        print_point(&point as *const Point); // Pass as pointer
    }
}

C Code:

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

void print_point(const Point* p) {
    printf("Point: x=%d, y=%d\n", p->x, p->y); // Access fields
}

Key Points:

  • #[repr(C)]: Ensures fields are laid out in declaration order, matching C’s struct layout.
  • Type Compatibility: Rust’s c_int maps to C’s int. Use std::os::raw or libc for type safety.
  • Safety: The C function must not dereference null pointers or modify read-only pointers (*const).
  • Ownership: The Rust struct remains owned by Rust unless explicitly transferred (e.g., with Box::into_raw).

Example with Mutation:

extern "C" {
    fn modify_point(p: *mut Point);
}

fn main() {
    let mut point = Point { x: 10, y: 20 };
    unsafe {
        modify_point(&mut point as *mut Point);
    }
    println!("x={}, y={}", point.x, point.y); // Modified by C
}

C Code:

void modify_point(Point* p) {
    p->x += 1;
    p->y += 1;
}

Best Practice:

  • Use #[repr(C)] for all structs passed to C.
  • Validate pointers in C (e.g., check for null).
  • Clearly document ownership and mutability rules.
  • Use bindgen to generate Rust bindings and cbindgen for C headers to ensure consistency.

Q111: Why do I feel further from the machine in Rust compared to C?

Feeling “further from the machine” in Rust compared to C is a common perception, especially for developers accustomed to C’s low-level control. This stems from Rust’s abstractions, safety guarantees, and stricter compiler checks, which can make it feel less direct than C’s raw memory manipulation. Here’s why and how to reconcile this:

  • Reasons for the Perception:

    1. Ownership and Borrowing:
      • Rust enforces strict ownership and borrowing rules at compile time, requiring developers to think about lifetimes and references (&T, &mut T).
      • C allows direct pointer manipulation without checks, feeling closer to raw memory.
      • Example:
        #![allow(unused)]
        fn main() {
        let mut x = 42;
        let r = &mut x; // Borrow, enforced by compiler
        *r = 100;
        }
        int x = 42;
        int* r = &x; // Direct pointer, no checks
        *r = 100;
        
    2. Abstractions:
      • Rust encourages high-level constructs like traits, iterators, and smart pointers (Box, Rc), which abstract away raw memory operations.
      • C relies on raw pointers, arrays, and manual memory management (malloc, free), giving a more direct feel.
      • Example: Rust’s Vec vs. C’s array:
        #![allow(unused)]
        fn main() {
        let v = vec![1, 2, 3]; // Managed by Vec
        }
        int* arr = malloc(3 * sizeof(int)); // Manual allocation
        
    3. Compiler Restrictions:
      • Rust’s borrow checker and type system prevent unsafe operations, requiring workarounds (e.g., unsafe or redesign) that feel less immediate.
      • C allows unrestricted memory access, which feels closer to the machine but risks undefined behavior.
    4. Standard Library:
      • Rust’s std provides safe abstractions (e.g., String, Vec), hiding low-level details.
      • C’s standard library (e.g., stdio.h, stdlib.h) is minimal, requiring manual memory handling.
  • Why Rust Is Still Close to the Machine:

    • Zero-Cost Abstractions: Rust’s abstractions (e.g., iterators, generics) compile to machine code as efficient as C’s, with no runtime overhead (see Q90).
    • Unsafe Rust: For cases needing raw control, Rust’s unsafe blocks allow pointer manipulation, FFI, and direct memory access, similar to C.
      #![allow(unused)]
      fn main() {
      unsafe {
          let ptr = std::alloc::alloc(std::alloc::Layout::new::<i32>());
          *(ptr as *mut i32) = 42; // Raw memory access
      }
      }
    • Performance: Rust matches C’s performance in systems programming (e.g., Firefox, AWS Firecracker), proving it’s just as “close” to the machine.
    • No Runtime: Like C, Rust has no garbage collector or heavy runtime, unlike Java or Python.
  • Reconciling the Feeling:

    • Learn Idioms: Rust’s ownership model feels restrictive initially but becomes intuitive with practice. Study The Rust Book to master borrowing.
    • Use Unsafe Sparingly: For low-level tasks, unsafe provides C-like control, but wrap it in safe abstractions.
    • Profile Code: Rust’s compiled output is as efficient as C’s; use tools like cargo-asm to inspect generated assembly.
    • Leverage Abstractions: Embrace Vec, String, and traits for productivity without sacrificing performance.

Best Practice:

  • Accept Rust’s safety constraints as a trade-off for fewer bugs.
  • Use unsafe only for specific low-level tasks (e.g., FFI, custom allocators).
  • Focus on Rust’s idioms (ownership, traits) to write code that’s both safe and close to the machine.
  • Compare Rust and C assembly output to confirm equivalent performance.