First time trying to do FFI bindings

So partially inspired by Rust and Unity game development - YouTube and later by Make a library · Issue #43 · 4JX/L5P-Keyboard-RGB · GitHub I decided to try giving it a go. So I'm currently figuring out how to make bindings for GitHub - 4JX/lenovo-legion-hid.

When I asked myself "how hard can it be" I naively thought I'd be able to go through it all without much thought (Surely spamming extern "C" everywhere would magically work). But things got a bit confusing.

A few questions that have come to my mind so far:

  • When calling Rust code from other languages I've often seen the use of pointers with a syntax similar to *const my_thing (like this). I assume that's because you're just instructing the Rust side of the program to read from "whatever value is at this location in memory" that has been created on the other language?

  • Types like Result get a warning about not being FFI safe. Given the concept of Result and Option doesn't really exist in other languages, is it a better option to unwrap it all away?

  • Similarly it also complains for [u8; N]. When I try to use *const [u8; N] I start to have problems with needing to wrap parts of my code into unsafe (which I can understand) as I need to for example use .as_ref() to be able to use .iter() and even then I get stuck at figuring out how to index the array, which leaves me wondering if there's a prettier solution for this.

  • Finally, is there an equivalent of Arc<AtomicBool> that I can use in this case (Rust to C#)? Mostly want to escape loops when needed from another thread.

Resources to aid me on this journey would also be appreciated.

No, most definitely not. Since C doesn't know about rich algebraic types like the Option and Result enums, you must perform manual error handling. Unwrapping everything will only make your program crash panic upon encountering None or Err. That's one of the worst possible things you can do while in FFI, it's way more unsafe even than interacting with FFI itself.

FFI is necessarily unsafe as Rust can't verify that your C code is safe. However, needing to wrap your own implementation into unsafe blocks is a code smell. You don't want to convert everything to raw pointers – FFI layers should be as thin as possible, they should not include or (worse yet) duplicate domain logic.

I don't get this question. Are you asking what pointers are useful for in general?

Should've phrased that better, but yeah came down to whether I'd need to do error handling on the Rust side only for Rust code.

First time hearing about "code smell" :thinking:. Will keep it in mind.

That was a more general question yes. Since I've never done this sort of thing before I was taking a guess at why the syntax was used.

Sorry I still don't understand. Are you asking why pointers in Rust have the *const T or *mut T syntax?

More generally why the use of a pointer rather than taking by value. Since simpler types like u8 don't seem to have this need.

Well, for the same reasons you wouldn't sometimes pass something by-value across Rust functions. Sometimes indirection is you need, sometimes you want to retain ownership, etc.

The *const and *mut syntax is for raw pointers. You will need those in FFI because many other programming languages explicitly expect pointers as arguments to function calls (or will return pointers).

But raw pointers can also be used in Rust when not involving FFI at all, e.g. when you want to have a reference in a struct which points to some other field inside the same struct (which is impossible in Rust when using safe references, thus using these techniques requires unsafe too).


A particular example where raw pointers are used is in the implementation of split_at_mut (or split_at_mut_unchecked in nightly Rust, you can click on [src] to see the source code), which is explained in more depth in the section Splitting Borrows in the Nomicon.

pub unsafe fn split_at_mut_unchecked(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
    let len = self.len();
    let ptr = self.as_mut_ptr();

    // SAFETY: Caller has to check that `0 <= mid <= self.len()`.
    //
    // `[ptr; mid]` and `[mid; len]` are not overlapping, so returning a mutable reference
    // is fine.
    unsafe { (from_raw_parts_mut(ptr, mid), from_raw_parts_mut(ptr.add(mid), len - mid)) }
}

That *const T is the syntax for a raw, immutable pointer to some T. Normally when you do FFI you'll be operating on raw pointers because other languages don't have any concept of Rust's references.

In the same way function items coerce to function pointers, a &T will coerce into a *const T and a &mut T will coerce into *mut T.

Other languages don't know anything about Rust's enums so you will need to figure out a FFI-safe way to signal errors to the other side.

A pretty common method used in C when you need to signal an error and also return a value is to accept an "output" pointer as one of your arguments and return an error enum. In Rust that might look something like this:

/// A normal Rust error type returned by your function.
enum Error {
    InvalidArgument,
}

fn function_being_wrapped(arg: f64) -> Result<f64, Error> {
    if arg.is_finite() {
        Ok(arg + 42.0)
    } else {
        Err(Error::InvalidArgument)
    }
}

/// A FFI-safe return code used to indicate the result of an operation.
#[repr(C)]
pub enum ReturnCode {
    Ok = 0,
    InvalidArgument = 1,
}

/// The actual function that will be called by C.
#[no_mangle]
pub extern "C" fn my_binding(arg: f64, out: *mut f64) -> ReturnCode {
    if out.is_null() {
        return ReturnCode::InvalidArgument;
    }

    let result = function_being_wrapped(arg);

    match result {
        Ok(value) => {
            unsafe {
                *out = value;
            }
            ReturnCode::Ok
        }
        Err(Error::InvalidArgument) => ReturnCode::InvalidArgument,
    }
}

(playground)

Technically, we should also wrap the function_being_wrapped() call in std::panic::catch_unwind() and return a ReturnCode::Panic to handle the case when your Rust function panics because unwinding across the FFI boundary is UB. If you use the same "validate arguments, make call, match to handle return values" pattern it should just be a case of adding tweaking the match statement.

The Arc type has a into_raw() method which returns a pointer you can pass around.

Keep in mind that C# won't know anything about Rust's destructors or clone() methods, so you'll need to expose functions that increment/decrement the reference count manually.

use std::{
    ffi::c_void,
    sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    },
};

/// Create a new reference counted flag that can be passed across the FFI
/// boundary.
///
/// The reference count must be explicitly decremented with
/// `atomic_bool_release()` whenever you are done with it. Similarly, if you
/// want to give a long-lived reference to another piece of code you should call
/// `atomic_bool_clone()` to get a new handle they can use.
///
/// Note that we deliberately return `*mut c_void` instead of `*mut bool` here
/// so the caller won't be tempted to do a non-atomic load.
#[no_mangle]
pub extern "C" fn atomic_bool_new() -> *mut c_void {
    let flag = Arc::new(AtomicBool::new(false));
    Arc::into_raw(flag) as *mut c_void
}

#[no_mangle]
pub unsafe extern "C" fn atomic_bool_get(ptr: *mut c_void) -> bool {
    let arc = Arc::from_raw(ptr as *const AtomicBool);
    arc.load(Ordering::SeqCst)
}

#[no_mangle]
pub unsafe extern "C" fn atomic_bool_set(ptr: *mut c_void, value: bool) {
    let arc: &AtomicBool = &*(ptr as *const AtomicBool);
    arc.store(value, Ordering::SeqCst);
}

/// Clone the `Arc<AtomicBool>`, incrementing its reference count and getting a
/// handle which can be used to access it.
#[no_mangle]
pub unsafe extern "C" fn atomic_bool_clone(ptr: *mut c_void) -> *mut c_void {
    Arc::increment_strong_count(ptr as *const AtomicBool);
    // Note: we return the same pointer instead of returning nothing because it
    // will push developers into treating another reference as a separate value
    // that must be cloned and freed.
    ptr
}

/// Drop the `Arc<AtomicBool>`, decrementing its reference count and freeing the
/// value if no other references exist.
#[no_mangle]
pub unsafe extern "C" fn atomic_bool_release(ptr: *mut c_void) {
    Arc::decrement_strong_count(ptr as *const AtomicBool);
}

(playground)

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.