Sling Academy
Home/Rust/Interfacing Rust “Objects” with C/C++: FFI and #[repr(C)] Structs

Interfacing Rust “Objects” with C/C++: FFI and #[repr(C)] Structs

Last updated: January 06, 2025

In recent years, Rust has gained significant traction in the programming world due to its powerful features such as safety and concurrency. However, integrating Rust with existing C/C++ codebases is crucial for many developers, as C/C++ still power a significant amount of systems and applications worldwide. This article delves into how to interface Rust's “objects” with C/C++ using Foreign Function Interface (FFI) and #[repr(C)] structs.

Understanding FFI in Rust

FFI, or Foreign Function Interface, allows for calling functions defined in other languages. In Rust's context, FFI is mainly used to interface with libraries written in C or C++, allowing the seamless utilization of existing legacy systems. Rust provides the extern keyword to define such interfaces.

Basic C Usage

Let’s start with a basic example of calling a simple C function from Rust. First, assume we have a C header file as follows:

// simple.h
#ifndef SIMPLE_H
#define SIMPLE_H

int add(int a, int b);

#endif

And its implementation:

// simple.c
#include "simple.h"

int add(int a, int b) {
    return a + b;
}

To integrate this C code with Rust, we need to define these functions in Rust. Here’s how you can do that:

// lib.rs
#[link(name = "simple")]
extern "C" {
    fn add(a: i32, b: i32) -> i32;
}

fn main() {
    unsafe {
        let result = add(2, 3);
        println!("The sum is {}", result);
    }
}

The #[link] attribute specifies the library name szhe extern "C" block declares foreign functions.

Using #[repr(C)] for Structs

Rust’s structs are laid out in memory in a different manner than C's structs. To ensure compatibility, you can use the #[repr(C)] attribute, which changes the layout of the struct to be compatible with C.

Here’s an example:

// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H

typedef struct {
    int x;
    float y;
} MyStruct;

void use_struct(MyStruct s);

#endif

To define the corresponding struct in Rust:

// lib.rs
#[repr(C)]
pub struct MyStruct {
    x: i32,
    y: f32,
}

extern "C" {
    fn use_struct(s: MyStruct);
}

fn main() {
    let s = MyStruct { x: 5, y: 6.1 };
    unsafe {
        use_struct(s);
    }
}

By annotating the struct with #[repr(C)], we ensure that MyStruct is laid out in the same way as the C compiler expects.

Interfacing with C++

Interfacing Rust with C++ is a little more complex due to name mangling and other features unique to C++. Here’s a brief introduction to dealing with this:

You will use extern "C" in C++ as well to avoid name mangling:

// example.cpp
extern "C" {
    int add(int a, int b);
}

int add(int a, int b) {
    return a + b;
}

Compile this with a C++ compiler and link it in your Rust project using a similar extern "C" block as demonstrated in the earlier C example. The principle of #[repr(C)] structs remains the same.

Practical Considerations

  • Safety: Interfacing with C/C++ bypasses some of Rust's safety checks, such as borrow checker, so be thorough with unsafe code audits.
  • Tooling: Tools like rust-bindgen can automatically generate Rust bindings for existing C/C++ code.
  • Platform Differences: Watch out for differences in calling conventions and data representation on different platforms.

By understanding and using Rust’s FFI and #[repr(C)] struct features, developers can seamlessly integrate Rust into C/C++ projects, fostering the modern advantages of Rust while maintaining access to well-established C/C++ libraries.

Next Article: Best Practices for Combining Rust’s Borrow Checker with OOP-Like Designs

Previous Article: Comparing Rust’s Trait Objects to Go’s Interfaces

Series: Object-Oriented Programming in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior