Calling Rust code from C

As we stated in the previous section, when Rust libraries expose their functions to other languages using the extern block, they expose the C ABI (cdecl) by default. As such, it becomes a very seamless experience of calling Rust code from C. To C, they appear just like regular C functions. We'll take a look at an example of calling Rust code from a C program. Let's create a cargo project for this by running cargo new rust_from_c --lib.

In our Cargo.toml file, we have the following items:

# rust_from_c/Cargo.toml

[package]
name = "rust_from_c"
version = "0.1.0"
authors = ["Rahul Sharma <[email protected]>"]
edition = "2018"

[lib]
name = "stringutils"
crate-type = ["cdylib"]

Under the [lib] section, we specified the crate as cdylib, which indicates that we want a dynamically loadable library to be generated, which is more commonly known as a shared object file (.so) in Linux. We specified an explicit name for our stringutils library, and this will be used to create the shared object file.

Now, let's move on to our implementation in lib.rs:

// rust_from_c/src/lib.rs

use std::ffi::CStr;
use std::os::raw::c_char;

#[repr(C)]
pub enum Order {
Gt,
Lt,
Eq
}

#[no_mangle]
pub extern "C" fn compare_str(a: *const c_char, b: *const c_char) -> Order {
let a = unsafe { CStr::from_ptr(a).to_bytes() };
let b = unsafe { CStr::from_ptr(b).to_bytes() };
if a > b {
Order::Gt
} else if a < b {
Order::Lt
} else {
Order::Eq
}
}

We have a single function, compare_str. We prepend it with the extern keyword to expose it to C, followed by specifying the "C" ABI for the compiler to generate code appropriately. We also need to add a #[no_mangle] attribute, as Rust adds random characters to function names by default to prevent the clashing of names of types and functions across modules and crates. This is called name mangling. Without this attribute, we won't be able to call our function by the name compare_str. Our function lexicographically compares two C strings passed to it and returns an enum, Order, accordingly, which has three variants: Gt (Greater than), Lt (Less than), and Eq (Equal). As you may have noticed, the enum definition has a #[repr(C)] attribute. Because this enum is being returned to the C side, we want it to be represented in the same way as a C enum. The repr attribute allows us to do that. On the C side, we will get a uint_32 type as the return type of this function as enums variants are represented as 4 bytes in Rust, as well as in C. Do note that at the time of writing this book, Rust follows the same data layout for enums that have associated data as it does for C enums. However, this may change in the future.

Now, let's create a file called main.c that uses our exposed function from Rust:

// rust_from_c/main.c

#include <stdint.h>
#include <stdio.h>

int32_t compare_str(const char* value, const char* substr);

int main() {
printf("%d ", compare_str("amanda", "brian"));
return 0;
}

We declared the prototype of our compare_str function, just like any normal prototype declaration. Following that, we called compare_str in main, passing in our two string values. Do note that if we were passing strings that were allocated on the heap, we would need to also free it from the C side. In this case, we are passing a C string literal that goes to the data segment of the process, and so we don't need to do any free calls. Now, we'll create a simple Makefile that builds our stringutils crate and also compiles and links with our main.c file:

# rust_from_c/Makefile

main:
cargo build
gcc main.c -L ./target/debug -lstringutils -o main

We can now run make to build our crate and then run main by first setting our LD_LIBRARY_PATH to where our generated libstringutils.so resides. Following that, we can run main like so:

$ export LD_LIBRARY_PATH=./target/debug
$ ./main

This gives us an output of 1, which is the value of the Lt variant from the Order enum on the Rust side. The takeaway from this example is that when you are invoking a Rust function from C/C++ or any other language that has a supported ABI in Rust, we cannot pass Rust-specific data types to the FFI boundary. For instance, passing Option or Result types, that ha've associated data with them is meaningless, as C cannot interpret and extract values out of them, as it has no way of knowing about that. In such cases, we need to pass primitive values as return types from functions to the C side or convert our Rust type to some format that C can understand.

Now, consider our previous case of calling C code from Rust. In the manual way, we needed to write extern declarations for all of our APIs that have been declared in header files. It would be great if this could be automated for us. Let's see how we can do that next!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset