DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Cover image for Calling C code from Rust
Yury Samkevich
Yury Samkevich

Posted on

Calling C code from Rust

Rust was originally designed as a systems language for writing high performance applications that are usually written in C or C++. Although Rust solves problems that C/C++ developers have been struggling with for a long time there are countless amount of C and C++ libraries and it makes no sense to replace all of them with Rust. For this reason, Rust makes it easy to communicate with C APIs without overhead, and to leverage its ownership system to provide much stronger safety guarantees for those APIs at the same time.

Rust provides a foreign function interface (FFI) to communicate with other languages. Following Rust's design principles, the FFI provides a zero-cost abstraction where function calls between C and Rust have identical performance to C function calls. FFI bindings can also leverage language features such as ownership and borrowing to provide a safe interface that enforces protocols around pointers and other resources.

In this post we'll explore how to call C code from Rust and to build C and Rust code together as well as how to automate this process. You can find the source code of the complete project on github.

Note: As C++ has no stable ABI for the Rust compiler to target, it is recommended to use C ABI for any interoperability between Rust and C++. Therefore, in this article we will talk about interacting with C code, because the concepts provided here cover both C and C++.

Creating bindings

We will start with a simple example of calling C code from Rust. Let's imagine that we have the following header file:

/* File: libvec/vec.h */
#pragma once

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

void scale(Vec2* vec, int factor);

Enter fullscreen mode Exit fullscreen mode

and corresponding C file:

/* File: libvec/vec.c */
#include "vec.h"

void scale(Vec2* vec, int factor) {
    vec->x *= factor;
    vec->y *= factor;
}

Enter fullscreen mode Exit fullscreen mode

Before we start using any C code from Rust, we need to define all data types and function signatures we are going to interact with in Rust. Usually in C/C++ world we use header files for that purpose, which define all necessary data. But in Rust, we need to either manually translate these definitions to Rust code, or use a tool to generate these definitions.

Let us write a rust file which resembles the C header file:

/* File: src/bindings.rs */
use std::os::raw::c_int;

#[repr(C)]
#[derive(Debug)]
pub struct Vec2 {
    pub x: c_int,
    pub y: c_int,
}

extern "C" {
    pub fn scale(
        vec: *mut Vec2,
        factor: c_int
    );
}
Enter fullscreen mode Exit fullscreen mode

Rust needs to laydown members in a structure the way C would laydown it in memory. For that reason we include the #[repr(C)] attribute, which instructs the Rust compiler to always use the same rules C does for organizing data within a struct.

We declared function scale without defining a body. So, we use extern keyword to indicate the fact that the definition of this function will be provided elsewhere or linked into the final library or binary from a static library.

In the extern block, we have a string, "C", following it. This "C" specifies that we want the compiler to respect the C ABI so that the function-calling convention follows exactly as a function call that's done from C. An Application Binary Interface(ABI) is basically a set of rules and conventions that dictates how types and functions are represented and manipulated at the lower levels. There are also other ABIs that Rust supports such as fastcall, cdecl, win64, and others, but this topic is beyond the scope of the current article.

Generating bindings

So far we manually wrote interface to interact with C. But this approach has some drawbacks. Consider real case scenario where each header file has lots of structure definitions, function declarations, inline functions etc. As well as writing interfaces manually is tedious and error prone. But luckily for us there is a tool called bindgen which can perform these conversions automatically.

We can install bindgen with the following command:

$ cargo install bindgen
Enter fullscreen mode Exit fullscreen mode

The command to generate bindings may look like this:

$ bindgen libvec/vec.h -o src/bindings.rs
Enter fullscreen mode Exit fullscreen mode

Although we can use bindgen as a command line tool because it seems to give you more control over generation, the recommended way to use bindgen is generating the bindings on the fly through build.rs script.

A build.rs script is a file written in Rust syntax, that is executed on your compilation machine, after dependencies of your project have been built, but before your project is built. If you want to know more about build.rs please refer to the documentation. To generate bindings with bindgen we add the build.rs file with the following content:

/* File: build.rs */
extern crate bindgen;
use std::path::PathBuf;

fn main() {
    // Tell cargo to invalidate the built crate whenever the wrapper changes
    println!("cargo:rerun-if-changed=libvec/vec.h");
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate bindings for.
        .header("libvec/vec.h")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        // Finish the builder and generate the bindings.
        .generate()
        .expect("Unable to generate bindings");

    // Write the bindings to the src/bindings.rs file.
    let out_path = PathBuf::from("src");
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

Enter fullscreen mode Exit fullscreen mode

We also need to add bindgen to build dependencies in Cargo.toml file:

[build-dependencies]
bindgen = "0.53.1"
Enter fullscreen mode Exit fullscreen mode

Now, when we run cargo build, our bindings are generated on the fly to src/bindings.rs file.

Finally, we can use our generated buildings in Rust as in the following example:

/* File: src/main.rs */
use crate::bindings::{scale, Vec2};

mod bindings;

fn main() {
    unsafe{
        let mut vec = Box::new(Vec2{ x: 5, y: 10 });
        scale(&mut *vec, 2);
        println!("scaled vector: {:?}", *vec);
    }
}

Enter fullscreen mode Exit fullscreen mode

Building C code

Since the Rust compiler does not directly know how to compile C code, we need to compile our non-Rust code ahead of time. We can start simple and compile it in static library, which then will be combined with our Rust code at the final linking step.

To compile static library libvec.a from our C code we can use the following command:

$ cc -c libvec/vec.c
$ ar rcs libvec.a vec.o
Enter fullscreen mode Exit fullscreen mode

Now we can link it all together:

$ rustc -l static=vec -L. src/main.rs
Enter fullscreen mode Exit fullscreen mode

This command tells Rust compiler to look for a static libvec.a in the current . path. This should compile and give you the main binary, which can be executed.

Using cc crate

It might be quite tedious to compile static library manually every time we make changes in C code. The better solution is to instead utilize the cc crate, which provides an idiomatic Rust interface to the compiler provided by the host.

In our case of compiling a single C file as a dependency to a static library, we can amend out build.rs script using the cc crate. The modified version would look like this:

/* File: build.rs */
extern crate bindgen;
extern crate cc;
use std::path::PathBuf;

fn main() {
    // Tell cargo to invalidate the built crate whenever the wrapper changes
    println!("cargo:rerun-if-changed=libvec/vec.h");
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate bindings for.
        .header("libvec/vec.h")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        // Finish the builder and generate the bindings.
        .generate()
        .expect("Unable to generate bindings");

    // Write the bindings to the src/bindings.rs file.
    let out_path = PathBuf::from("src");
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    // Build static library
    cc::Build::new()
        .file("libvec/vec.c")
        .compile("libvec.a");
}

Enter fullscreen mode Exit fullscreen mode

Updated build dependencies section in Cargo.toml would look like this:

[build-dependencies]
bindgen = "0.53.1"
cc = "1.0"
Enter fullscreen mode Exit fullscreen mode

Now we can simply run cargo build to generate bindings, compile static library and link it with a Rust code. How cool is that!

Using CMake

In real world we usually have existing C projects that often use popular build systems like make or CMake. And it would be very inconvenient to use cc crate to compile them and don't have an option to reuse existing makefiles. Likely for us Rust have cmake crate that is intended to help us.

Let's assume that our vec library uses CMake and we have the following CMakeLists.txt file:

# File: libvec/CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(libvec C)

add_library(vec STATIC vec.c)

install(TARGETS vec DESTINATION .)
Enter fullscreen mode Exit fullscreen mode

To use cmake crate we need to add build dependency on it to Cargo.toml file:

[build-dependencies]
bindgen = "0.53.1"
cmake = "0.1.48"
Enter fullscreen mode Exit fullscreen mode

And modify our build.rs file as follows:

/* File: build.rs */
extern crate bindgen;
extern crate cmake;
use cmake::Config;
use std::path::PathBuf;

fn main() {
    // Tell cargo to invalidate the built crate whenever the wrapper changes
    println!("cargo:rerun-if-changed=libvec/vec.h");
    let bindings = bindgen::Builder::default()
        // The input header we would like to generate bindings for.
        .header("libvec/vec.h")
        // Tell cargo to invalidate the built crate whenever any of the
        // included header files changed.
        .parse_callbacks(Box::new(bindgen::CargoCallbacks))
        // Finish the builder and generate the bindings.
        .generate()
        .expect("Unable to generate bindings");

    // Write the bindings to the src/bindings.rs file.
    let out_path = PathBuf::from("src");
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");

    // Build static library
    let dst = Config::new("libvec").build();
    println!("cargo:rustc-link-search=native={}", dst.display());
    println!("cargo:rustc-link-lib=static=vec");
}

Enter fullscreen mode Exit fullscreen mode

We use cmake::Config type to trigger CMake driven to build our library, telling that itโ€™s code and CMakeFiles.txt files are located in libvec subdirectory and then requesting the build to happen. Next lines write to stdout special command for Cargo to set library search path and pick our libvec.a for linking respectively. After that we can compile our project with cargo build.

Summary

We've talked today about Rust and C interoperability, how to generate bindings to C code and how to automate this process as well as how to use cc and CMake crates to build C code from cargo.

I hope you have found the information above useful and helpful, any questions and feedbacks and welcomed.
If you find this article interesting don't forget to like it and give a start to source code.

Top comments (3)

Collapse
kgrech profile image
Konstantin Grechishchev

What is the output of the

rustc -l static=vec -L. src/main.rs
Enter fullscreen mode Exit fullscreen mode

command?

Is it executable or is it an object file? If it is an object file, what is the format of it?

It is possible to compile multiple rs files and link them to executable without using cargo?

Collapse
samkevich profile image
Yury Samkevich Author

This command outputs executable. rustc has an option --emit obj to output an object file. I believe the format of object file depends on platform. On macOS it's Mach-O format.
Compilation unit in rust is crate, not file, so It seems possible to compile multiple crates and link them in executable with standard linker (like ld), but you also need to link rust std somehow (if you use it).

Collapse
sectasy0 profile image
Piotrek M. • Edited on

Hi, Great article! I'have made a similar one about writing python modules in rust, would be great if you take a look and give a feedback what you thing about it, thanks! โค๏ธ

๐ŸŒš Browsing with dark mode makes you a better developer.

It's a scientific fact.