Posted on

The Rust programming language is considered as one of the potential successors for C++ when it comes to systems programming. A reasonable first step, when adapting to a new language, is replacing small parts of a system while the remaining parts are maintained in the former language.

To achieve this, an interoperability between the new and old language needs to be established. In the following, I will describe how to implement a dynamic Rust library that exposes its interface to C/C++.

Make a Rust library accessible from C/C++

Assume working in a cargo directory that was created e.g. by

cargo new --lib mylib

Use the FFI

Rust relies on the Foreign Function Interface (FFI) to interface with other languages. The FFI mainly targets C as language.

To make functions in Rust code accessible via the FFI, they need to be externalized and declared as no_mangle:

// src/lib.rs

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
  • extern "C" ensures the C ABI is used for calling functions in shared libraries
  • #[no_mangle] ensures the function names don't experience mangling

Both conditions must be fulfilled to enable interfacing via this function using the FFI.

Build a dynamic library

By default, cargo does not build dynamic system libraries. To generate a dynamic library (.dylib on Mac, .so in Linux and .dll on Windows), the Cargo.toml has to be extended by:

# Cargo.toml
[lib]
crate-type   = ["cdylib"]

This ensures rustc actually compiles a dynamic library.

Generate a C/C++ header file

To access functions in third-party libraries, C/C++ code needs to include a header file that declares the interface towards the library.

The header file can be written and maintained by hand, or be generated automatically by using cbindgen. Add cbindgen to the build dependencies in Cargo.toml:

# Cargo.toml
[build-dependencies]
cbindgen = "0.24.3"

And place a custom build.rs file in the project root with the following content:

// build.rs

extern crate cbindgen;

use std::env;

fn main() {
    let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    println!("Generating C/C++ header");
    cbindgen::Builder::new()
      .with_crate(crate_dir)
      .generate()
      .expect("Unable to generate bindings")
      .write_to_file("include/mylib.h");
}

This will ensure when calling cargo build, cbindgen is used to auto-generate a C/C++ header file that declares the interface of the Rust library.

After invoking cargo build --release, the generated header looks like this:

// include/mylib.h

#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>

extern "C" {

int32_t add(int32_t a, int32_t b);

} // extern "C"

Building the C++ project

Assume we want to use the Rust library in a C++ project:

// main.cpp
#include <add_rs.h>
#include <iostream>

int main()
{
    int result = add(3, 5); // `add` is implemented in Rust!
    std::cout << "Result: " << result << std::endl;
    return 0;
}

To compile this project with GCC, e.g. use:

g++ main.cpp -o add -Imylib/include -Lmylib/target/release/ -ladd_rs

Now, if you execute the program, the C++ program will interface with the library written in Rust using a C interface to calculate the desired result:

Result: 8

If you want to see a comprehensive proof on concept using Conan instead of plain GCC, you can find that on my GitHub.


Copyright © 2024 Sebastian Müller. All rights reserved.