Generate C API for struct methods with opaque pointers

EDIT: Changed function thing_count as suggested by @bjorn3.

Hello,

I want to compile a dynamic library exposing a C API. Let's say I have the following struct and some methods defined on it:

pub struct Thing {
  counter: u8
}

impl Thing {
    pub fn new(count: u8) -> Self {
        Self { counter: count }
    }

    pub fn count(&self) -> u8 {
        return self.counter
    }
}

As far as I understand, an idiomatic C API using opaque pointers would look somewhat like this:

#[no_mangle]
pub extern "C" fn thing_new(count: u8) -> *mut Thing {
  let boxed_a = Box::new(Thing::new(count));
  Box::into_raw(boxed_a)
}

#[no_mangle]
pub unsafe extern "C" fn thing_count(thing: *mut Thing) -> u8 {
    let ref_thing = &*thing;
    ref_thing.count()
}

#[no_mangle]
pub unsafe extern "C" fn thing_free(thing: *mut Thing) {
  drop(Box::from_raw(thing));
}

Once I have this API, I could use the various available binding generators for FFI. Since the API design follows a clearly recognizable pattern, I am looking for ways to generate the API from the method definition above - e.g. with a procedural macro annotating the impl block.

Before I start hacking away, I'd like to ask the community if a solution for this problem already exists. Any suggestions or hints would be very welcome :slight_smile:

Thank you and have a nice Sunday!

Using Box::from_raw here will deallocate the box at the end of the function. You should rather use &*thing.

Not sure if it already exists. I don't know of any such crate, but I wouldn't be surprised if someone else had already thought of the same.

1 Like

Thank you for the correction regarding Box::from_raw. I took the liberty to modify my original post in order to avoid confusing others who might stumble across those code samples.

Would you say it is also appropriate to do a null check in thing_count and thing_free? If yes, what would be the idiomatic return value of thing_count?

You can if you want, but it's perfectly fine to simply write "this pointer must not be null" on the documentation. If the absence of the value isn't meaningful, feel free to either ignore the possibility or do a null check and then print an error message and abort on failure.

It depends. Normally, I wouldn't bother because receiving null means the caller has done something wrong and the resulting segfault will point them to the offending line - kinda like an assertion with a bad error message.

As a general piece of advice, I'd recommend not trying to introduce macros or higher-level abstractions for your FFI code.

The FFI boundary is probably the most error-prone place to write unsafe code because the compiler can't make any guarantees that your caller is doing things correctly (or even that their signature matches yours!). Whenever you run into bugs, the first place you'll check is this area, and "clever" code is harder to debug than boring code because you need to mentally unroll all the abstractions.

I'd much rather maintain 1000 lines of boring unsafe code than 200 lines of "clever" code which tries to abstract away the copy-pasta.

That said, the safer_ffi crate exposes a couple of nice proc-macros for generating headers and generating FFI bindings to safe Rust functions.

This is one of the examples from their quickstart guide.

/// A `struct` usable from both Rust and C
#[derive_ReprC]
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Point {
    x: f64,
    y: f64,
}

/* Export a Rust function to the C world. */
/// Returns the middle point of `[a, b]`.
#[ffi_export]
fn mid_point(a: &Point, b: &Point) -> Point {
    Point {
        x: (a.x + b.x) / 2.,
        y: (a.y + b.y) / 2.,
    }
}

The way headers are generated isn't very amenable to cross-compilation, but other than that it's pretty nice. I'd probably do it from a Rust Analyzer-style self-updating test that generates headers for each of the platforms I'm distributing on, but it's the same idea.

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.