loading...
Cover image for How to call Rust functions from C on Linux

How to call Rust functions from C on Linux

dandyvica profile image Alain Viguier ・5 min read

Calling Rust from C is nor complicated neither really straightforward. I decided to experiment this feature with a list of functions callable from C for nearly each possible C type.

The Rust module is compiled as a dynamic library, by setting the [lib] tag in the Cargo.toml file:

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

The C file is compiled as a 64-bit executable, dynamically linked at runtime with the previous library.

All C-types have their equivalence in the Rust type system, using the FFI module or the libc crate (not used here).

Rust functions

All Rust functions should be marked as #[no_mangle] and extern because they will be marked as exported in the resulting library.

The Rust source file is using a simple macro to define most of the functions (this macro is found in the Rust book) to avoid repetitive code:

use std::ffi::{c_void, CStr};
use std::os::raw::c_char;
use std::slice;

// A Rust struct mapping the C struct
#[repr(C)]
#[derive(Debug)]
pub struct RustStruct {
    pub c: char,
    pub ul: u64,
    pub c_string: *const c_char,
}

macro_rules! create_function {
    // This macro takes an argument of designator `ident` and
    // creates a function named `$func_name`.
    // The `ident` designator is used for variable/function names.
    ($func_name:ident, $ctype:ty) => {
        #[no_mangle]
        pub extern "C" fn $func_name(v: $ctype) {
            // The `stringify!` macro converts an `ident` into a string.
            println!(
                "{:?}() is called, value passed = <{:?}>",
                stringify!($func_name),
                v
            );
        }
    };
}

// create simple functions where C type is exactly mapping a Rust type
create_function!(rust_char, char);
create_function!(rust_wchar, char);
create_function!(rust_short, i16);
create_function!(rust_ushort, u16);
create_function!(rust_int, i32);
create_function!(rust_uint, u32);
create_function!(rust_long, i64);
create_function!(rust_ulong, u64);
create_function!(rust_void, *mut c_void);

// for NULL-terminated C strings, it's a little bit clumsier
#[no_mangle]
pub extern "C" fn rust_string(c_string: *const c_char) {
    // build a Rust string from C string
    let s = unsafe { CStr::from_ptr(c_string).to_string_lossy().into_owned() };

    println!("rust_string() is called, value passed = <{:?}>", s);
}

// for C arrays, need to pass array size
#[no_mangle]
pub extern "C" fn rust_int_array(c_array: *const i32, length: usize) {
    // build a Rust array from array & length
    let rust_array: &[i32] = unsafe { slice::from_raw_parts(c_array, length as usize) };
    println!(
        "rust_int_array() is called, value passed = <{:?}>",
        rust_array
    );
}

#[no_mangle]
pub extern "C" fn rust_string_array(c_array: *const *const c_char, length: usize) {
    // build a Rust array from array & length
    let tmp_array: &[*const c_char] = unsafe { slice::from_raw_parts(c_array, length as usize) };

    // convert each element to a Rust string
    let rust_array: Vec<_> = tmp_array
        .iter()
        .map(|&v| unsafe { CStr::from_ptr(v).to_string_lossy().into_owned() })
        .collect();

    println!(
        "rust_string_array() is called, value passed = <{:?}>",
        rust_array
    );
}

// for C structs, need to convert each individual Rust member if necessary
#[no_mangle]
pub unsafe extern "C" fn rust_cstruct(c_struct: *mut RustStruct) {
    let rust_struct = &*c_struct;
    let s = CStr::from_ptr(rust_struct.c_string)
        .to_string_lossy()
        .into_owned();

    println!(
        "rust_cstruct() is called, values passed = <{} {} {}>",
        rust_struct.c, rust_struct.ul, s
    );
}

A mere cargo build will compile this lib.rs file and place the result in target/debug subdirectory of the Rust project directory. My lib is called librustcalls.so

You can look at exported functions with the nm command:

$ nm --defined-only  -D ./rustcalls/target/debug/librustcalls.so 
000000000000b5e0 T rust_char
000000000000b3b0 T rust_cstruct
0000000000016740 T rust_eh_personality
000000000000ba20 T rust_int
000000000000b140 T rust_int_array
000000000000bc40 T rust_long
000000000000b800 T rust_short
000000000000b010 T rust_string
000000000000b240 T rust_string_array
000000000000bb30 T rust_uint
000000000000bd50 T rust_ulong
000000000000b910 T rust_ushort
000000000000be60 T rust_void
000000000000b6f0 T rust_wchar

C functions

C function declarations are necessary to link the external library (at runtime) compiled with Rust.

Compilation is done by:

$ gcc -g call_rust.c -o call_rust -lrustcalls -L./rustcalls/target/debug

where:

  • -g is for including symbols for debugging with gdb
  • -o call_rust will create call_rust as the executable file
  • -lrustcalls tells the compiler to look for function definitions in the librustcalls.so shared lib file
  • -L./rustcalls/target/debug tells the compiler to look into this directory for the previous file

call_rust.c is this file:

// compile with gcc -g call_rust.c -o call_rust -lrustcalls -L./rustcalls/target/debug

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>

// sample struct to illustrate passing a C-struct to Rust
struct CStruct {
    char c;
    unsigned long ul;
    char *s;
};

// functions called in the Rust library
extern void rust_char(char c);
extern void rust_wchar(wchar_t c);
extern void rust_short(short i);
extern void rust_ushort(unsigned short i);
extern void rust_int(int i);
extern void rust_uint(unsigned int i);
extern void rust_long(long i);
extern void rust_ulong(unsigned long i);
extern void rust_string(char *s);
extern void rust_void(void *s);
extern void rust_int_array(const int *array, int length);
extern void rust_string_array(const char **array, int length);
extern void rust_cstruct(struct CStruct *c_struct);

int main() {
    // pass char to Rust
    rust_char('A');
    rust_wchar(L'ΞΆ');

    // pass short to Rust
    rust_short(-100);
    rust_ushort(100);

    // pass int to Rust
    rust_int(-10);
    rust_uint(10);

    // pass long to Rust
    rust_long(-1000);
    rust_ulong(1000);    

    // pass a NULL terminated string
    rust_string("hello world");

    // pass a void* pointer
    void *p = malloc(1000);
    rust_void(p);  

    // pass an array of ints
    int digits[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 
    rust_int_array(digits, 10);

    // pass an array of c strings
    const char *words[] = { "This", "is", "an", "example"};
    rust_string_array(words, 4);   

    // pass a C struct
    struct CStruct c_struct;
    c_struct.c = 'A';
    c_struct.ul = 1000;
    c_struct.s = malloc(20);
    strcpy(c_struct.s, "0123456789");
    rust_cstruct(&c_struct);

    // don't forget to clean up ;-)
    free(p); 
    free(c_struct.s);

    return 0;
}

You can look at linked libraries using the ldd command:

$ ldd call_rust
        linux-vdso.so.1 (0x00007ffe6d69c000)
        librustcalls.so => not found
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb695054000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fb695648000)

librustcalls.so is reported not found because not in the standard paths for shared libs. It's necessary to tell the linker where to find this file, using the LD_LIBRARY_PATH environment variable:

$ LD_LIBRARY_PATH=rustcalls/target/debug ldd call_rust
        linux-vdso.so.1 (0x00007ffcb8f62000)
        librustcalls.so => rustcalls/target/debug/librustcalls.so (0x00007f60a975b000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f60a936a000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f60a9166000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f60a8f5e000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f60a8d3f000)
        libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f60a8b27000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f60a9b97000)

Now to execute:

$ LD_LIBRARY_PATH=rustcalls/target/debug ./call_rust  
"rust_char"() is called, value passed = <'A'>
"rust_wchar"() is called, value passed = <'ΞΆ'>
"rust_short"() is called, value passed = <-100>
"rust_ushort"() is called, value passed = <100>
"rust_int"() is called, value passed = <-10>
"rust_uint"() is called, value passed = <10>
"rust_long"() is called, value passed = <-1000>
"rust_ulong"() is called, value passed = <1000>
rust_string() is called, value passed = <"hello world">
"rust_void"() is called, value passed = <0x55f0a68f17d0>
rust_int_array() is called, value passed = <[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]>
rust_string_array() is called, value passed = <["This", "is", "an", "example"]>
rust_cstruct() is called, values passed = <A 1000 0123456789>

Hope this helps !

Photo by Deanna Ritchie on Unsplash

Posted on by:

dandyvica profile

Alain Viguier

@dandyvica

Senior software engineer & sysadmin, technical architect, Linux expert. Always willing to learn new stuff.

Discussion

pic
Editor guide