DEV Community

Cover image for How to call Rust functions from C on Linux
Alain Viguier
Alain Viguier

Posted on

How to call Rust functions from C on Linux

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"]
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Hope this helps !

Photo by Deanna Ritchie on Unsplash

Discussion (1)

Collapse
jmgbsas profile image
jmgbsas

Finally a complete example and that worked for me. Thanks!