Rust and C with Cargo and CMake together

I was playing recently with Rust and one of the feature it supports is ability to link to C code. This post demonstrates the steps to do so, using Cargo as a primary build tool for Rust part of the code, invoking CMake to do the native C code compilation, producing a static library that Rust picks up and calls.

Things you need

Your machine should have:

  • Rust and Cargo installed (I’m using nighly toolchain),
  • CMake, GNU Make as well as GCC installed,

The receipe is pretty generic, not sensitive to particular versions of software, but I’m displaying the ones I’ve used on my Mac for a reference:

rustc --version
rustc 1.28.0-nightly (e3bf634e0 2018-06-28)

cargo --version
cargo 1.28.0-nightly (e2348c2db 2018-06-07)

cmake --version
cmake version 3.10.2
CMake suite maintained and supported by Kitware (kitware.com/cmake).

make --version
GNU Make 3.81
Copyright (C) 2006  Free Software Foundation, Inc.

gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.1.0 (clang-902.0.39.1)
Target: x86_64-apple-darwin17.5.0
Thread model: posix

Project skeleton

we can start with creating simple hello world project for rust using Cargo:

cargo new --bin rust-and-cmake

This will give us the following simple layout within rust-and-cmake dir:

.
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

Then, let’s create simple static library in C, within libfoo subdirectory, with the source file foo.c like this:

#include <stdio.h>

void testcall(float value)
{
    printf("Hello, world from C! Value passed: %f\n",value);
}

as well as having the following CMakeLists.txt as a build script:

cmake_minimum_required(VERSION 3.0)
project(LibFoo C)

add_library(foo STATIC foo.c)

install(TARGETS foo DESTINATION .)

There is one bit in CMakeLists.txt file that is not obvious, but needed - Cargo will rely on it later - install(TARGETS ... exactly pointing to . as destination.

The capabilities of this library are surely not impressive, but at least look clear.

The layout of the project should be now like this:

.
├── Cargo.lock
├── Cargo.toml
├── libfoo
│   ├── CMakeLists.txt
│   └── foo.c
└── src
    └── main.rs

Making Cargo doing the work

Let’s add some extra bits to the Caro to make things happen. We need to add the following lines to Cargo.toml file:

[package]
...
build="build.rs"

as well as new section:

[build-dependencies]
cmake = "0.1.31"

This will tell cargo that it will be using cmake crate as extra dependency for the build as well as that you will be using custom build extension, which is a Rust code in build.rs file. Make it look like this:

extern crate cmake;
use cmake::Config;

fn main()
{
    let dst = Config::new("libfoo").build();       

    println!("cargo:rustc-link-search=native={}", dst.display());
    println!("cargo:rustc-link-lib=static=foo");    
}

First line is rather straightofoward - declaring crate to be used. Then yoy bring cmake::Config type into scope. In the main function, you will use this type to trigger CMake driven build of your library, telling that it’s code and CMake files are located in libfoo subdirectory , then requesting the build to happen. And this is exactly what this line is doing:

    let dst = Config::new("libfoo").build();       

Next lines write to stdout special command for Cargo to set library search path and pick your libfoo.a for linking respectively. Pay attention to naming conventions where lib prefix and .a suffix are ommited, pretty much in CMakeList.txt files.

When you run cargo build -vv you will see some detailed output of how the machinery workes together.

First things you see is Cargo pulling dependencies: cmake and dependent cc crates in this case. That’s expected to I guess anyone who worked with Cargo already. You may also check that dependency on the cmake crate website here.

...
Compiling cc v1.0.17
    Running `rustc --crate-name cc /Users/adam/.cargo/registry/src/github.com-1ecc6299db9ec823/cc-1.0.17/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 -C metadata=e1bf408b0753b950 -C extra-filename=-e1bf408b0753b950 --out-dir /Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/deps -L dependency=/Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/deps --cap-lints warn`
Compiling cmake v0.1.31
    Running `rustc --crate-name cmake /Users/adam/.cargo/registry/src/github.com-1ecc6299db9ec823/cmake-0.1.31/src/lib.rs --crate-type lib --emit=dep-info,link -C debuginfo=2 -C metadata=e2a8cfacdcc8b350 -C extra-filename=-e2a8cfacdcc8b350 --out-dir /Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/deps -L dependency=/Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/deps --extern cc=/Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/deps/libcc-e1bf408b0753b950.rlib --cap-lints warn`
...

Then down the lines you’ll see a classical CMake diagnostic output:

     Running `/Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/build/rust-and-c-685abe0ebdbf121d/build-script-build`
running: "cmake" "/Users/adam/eclipse-workspace-rust/rust-and-c/libfoo" "-DCMAKE_INSTALL_PREFIX=/Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/build/rust-and-c-73d11683aacb7aa5/out" "-DCMAKE_C_FLAGS= -ffunction-sections -fdata-sections -fPIC -m64" "-DCMAKE_C_COMPILER=/usr/bin/cc" "-DCMAKE_CXX_FLAGS= -ffunction-sections -fdata-sections -fPIC -m64" "-DCMAKE_CXX_COMPILER=/usr/bin/c++" "-DCMAKE_BUILD_TYPE=Debug"
-- The C compiler identification is AppleClang 9.1.0.9020039
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Configuring done
-- Generating done
CMake Warning:
  Manually-specified variables were not used by the project:

    CMAKE_CXX_COMPILER
    CMAKE_CXX_FLAGS


-- Build files have been written to: /Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/build/rust-and-c-73d11683aacb7aa5/out/build
running: "cmake" "--build" "." "--target" "install" "--config" "Debug" "--"
Scanning dependencies of target foo
[ 50%] Building C object CMakeFiles/foo.dir/foo.c.o
[100%] Linking C static library libfoo.a
[100%] Built target foo
Install the project...
-- Install configuration: "Debug"
-- Installing: /Users/adam/eclipse-workspace-rust/rust-and-c/target/debug/build/rust-and-c-73d11683aacb7aa5/out/./libfoo.a

Last line is interesting and worth paying attention for, when comes to troubleshooting. It tells where the library was installed (in local Cargo’s build directory) and this should finall match with the library search path you’re providing in build.rs file, getting it from dst.display(). This is where two worlds meet, by (more or less officially) agreed convention.

Calling the library

Ok, time to consume the library code from Rust. Let’s add few lines and modify default Hello World example you have so far, to this one:

#[link(name="foo", kind="static")]
extern { 
    // this is rustified prototype of the function from our C library
    fn testcall(v: f32); 
}

fn main() {
    println!("Hello, world from Rust!");

    // calling the function from foo library
    unsafe { 
        testcall(3.14159); 
    };
}

The initial section declares an external function. Note that this prototype requires a bit of manual conversion from C prototype to Rust one. It’s straightforward for simple functions operating on primitive value types, but might be more difficult to craft when more complex data types are involved.

Running the program

Once compiler and cargo run you should now see a nice program outputs on the stdout:

Hello, world from Rust!
Hello, world from C! Value passed: 3.141590

How about release build

You could spot that the cmake buld was invoked with -DCMAKE_BUILD_TYPE=Debug flag, which produced Debug variant of your libfoo.a. cmake crate is integrated smartly enough that when you build a release variant of your project with Cargo:

cargo build --release -vv

you will see that proper flags are propagated down properly and a CMake build is also producing release version of the library:

-- The C compiler identification is AppleClang 9.1.0.9020039
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
...
[100%] Built target foo
Install the project...
-- Install configuration: "Release"
...

Incremental builds

It’s worth mentioning that the integration with Cargo seem to nicely support incremental builds, so each cargo build exection will properly re-compile the native code with CMake whenever there were any changes to source code there, and will re-link the final executable to reflect those changes.

I hope that recipe is useful. You can download the example code from a git repository here. Note that is has some extra comments and mainenance files though.

comments powered by Disqus