Rapid prototyping C applications with Rust

4 minute read

Cargo is a gorgeous tool when it comes to building, packaging and shipping your own Rust applications. Some of you guys are living in a C/C++ world where developing Rust applications is just a hobby. A reason could be that Rust is currently not that highly accepted in the world of applications in production. This is sad, but even within your day to day development it could be possible to use Rust for a lots of tools or prototyping scenarios!

One example could be to use the possibilities of Rust and Cargo to do prototyping of C based applications. So how could we do that?

For the first try we simply build a new cargo project:

> cargo new --bin rapidc Created binary (application) `rapidc` project

The target is now to come as fast as possible back into the C world. For this we add the two dependencies libc and gcc , as well as a custom build script build.rs to our Cargo.toml file:

[package] name = "rapidc" version = "0.1.0" build = "build.rs" [dependencies] libc = "0" [build-dependencies] gcc = "0"

What else? For sure we need an entry point for our C application, which we name src/main.c and looks like:

#include <stdio.h> int main_c ( int argc , char * argv []) { printf ( "Found %d args:

" , argc ); for ( int i = 0 ; i < argc ; ++ i ) { printf ( " \t - %s

" , argv [ i ]); } return 0 ; }

This program does not do really special at all, but as a demonstration we print out all available command line arguments. The interesting part happens within the Rust entry function in main.rs :

extern crate libc ; use std :: env ; use std :: ffi :: CString ; use libc ::{ c_int , c_char }; extern "C" { fn main_c ( argc : c_int , argv : * const * const c_char ) -> c_int ; } fn main () { // Get the current args and map them to a vector of zero // terminated c strings: let args : Vec < CString > = env :: args () .filter_map (| arg | { CString :: new ( arg ) .ok () }) .collect (); // Convert these c strings to raw pointers let c_args : Vec <* const c_char > = args .iter () .map (| arg | { arg .as_ptr () }) .collect (); // Call the main function within the created c library unsafe { main_c ( c_args .len () as c_int , c_args .as_ptr ()); }; }

The Foreign Function Interface (FFI) of Rust is pretty straight forward and Cargo even supports us in writing those applications. We simply need to declare our C function within the scope of extern "C" { ... } . Since we cannot use the basic C types within Rust we have to use the ones of the libc crate (like c_int or c_char ). The main function creates a Vector of C pointers to our command line arguments and passes them within the unsafe {...} block to our C entry function.

Do we forgot something? Yes, what about the build.rs script? How can we tell Cargo to first build our C library and then link it to the Rust application? The build script looks like:

extern crate gcc ; fn main () { gcc :: Config :: new () .file ( "src/main.c" ) .include ( "src" ) .compile ( "libmain.a" ); }

Pretty easy right? The gcc crate helps us to easily build the main.c to the library libmain.a . Now we are able to try out our work by executing:

> cargo run cargo run Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/rapidc` Found 1 args: - target/debug/rapidc

Or what about:

> cargo run arg1 arg2 arg3 Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/rapidc arg1 arg2 arg3` Found 4 args: - target/debug/rapidc - arg1 - arg2 - arg3

Great, everything works as expected, …but stop! What happened here? We did not tell Cargo to link against a library called libmain.a . How should Cargo know what to do with the library? That is one of the very important features of such a great build environment: Strong defaults will lead into a better world. Cargo will simply assume that you call your library libmain.a for the file main.c .

If we want to link another library to our C application we can simply use the link directive:

# [ link ( name = "pthread" )] extern "C" { fn main_c ( argc : c_int , argv : * const * const c_char ) -> c_int ; }

It is pretty brilliant: On one hand we have the full power of Rust and Cargo via the language itself within the build.rs script. This means we have the full flexibility like in CMake by the usage of the complete programming language set of Rust. On the other hand the strong defaults of Cargo will make it very easy to build release versions, debug our code via gdb or lldb , package the application and write benchmarks or unit tests for your C functions. This is all possible without the need of a cruel Makefile or a bloated CMake environment. How great is that?! 😀

Currently I am working on some Cargo port called ‘Craft’ which should combine both worlds out of the box. Replacing the compiler facade is a lots of work and Cargo is even evolving very fast which makes me think about writing just an cargo extension to do faster prototyping in C. What do you think?

The full source code of the small example project is available on GitHub.