Exposing Rust instance from C

Hi, I'm working in the limit of my Rust-foo here so looking for advice if I'm totally off but I'm trying to create an instance from a C API, return a pointer so I can use that to add some values to a vector. I would like to keep a ptr to the instance so when it goes out of scope I can have the drop function clean things up.

It doesn't seem like I can set the ptr to null() to begin with and then set to self. Is there a different way to do this?

pub struct Foo {
    floats: Vec<f32>
    ptr: *mut Foo,
}

impl Foo  {
    pub unsafe extern "C" fn create_foo(
        out_ptr: *mut *mut Foo,
    ) {
        let foo = (Foo {
            floats: Vec::new(),
            ptr: null(),
        }) {
            let handle = Box::into_raw(Box::new(foo));
            foo.ptr = handle;
            unsafe {
                foo.ptr = handle
                *out_ptr = handle;
            }
    }

    pub unsafe extern "C" fn foo_add(
        &mut self,
        value: f32,
    ) {
        self.floats.push(value);
    }

impl Drop for Foo {
    fn drop(&mut self) {
        unsafe {
            drop(Box::from_raw(self.ptr));
        }
    }
}

It seems like you are trying to make sure your Foo automatically drops itself when it goes out of scope, but that's kinda backwards. For one, your drop(Box::from_raw(self.ptr)) line will lead to infinite recursion because dropping a Foo will cause it to try to drop itself, which will trigger the drop again.

However, you have a bigger problem - you are assuming that once you pass an object to C, your Rust code will still have control of it and be able to destroy it.

The better way to do things is to give C a way to explicitly free your Foo when it is no longer needed. This makes your life simpler because there's no need for self-pointers or fancy Drop implementations, and it lines up with how C APIs normally work.

#[derive(Default)]
pub struct Foo {
  floats: Vec<f32>,
}

#[no_mangle]
pub extern "C" fn create_foo() -> *mut Foo {
  let foo = Box::new(Foo::default());
  Box::into_raw(foo)
}

#[no_mangle]
pub unsafe extern "C" fn destroy_foo(foo: *mut Foo) {
  let _ = Box::from_raw(foo);
}

#[no_mangle]
pub unsafe extern "C" fn foo_add(foo: *mut Foo, value: f32) {
  let foo = &mut *foo;
  foo.floats.push(value);
}

You'll also notice that all my functions are standalone functions, rather than methods. That's because C has no concept of methods, so putting them inside an impl block probably won't do what you expect.

I've also added the #[no_mangle] attribute so the linker will be able to find the function symbols. Without it, the functions would probably be called something like my_crate::foo_add::abcd1234, where abcd1234 is some arbitrary hash that's derived from your crate's source code.

This code would then be used from C like so...

typedef struct Foo Foo;

Foo *create_foo();
void foo_add(Foo *foo, float value);
void destroy_foo(Foo *foo);

int main() {
  Foo *foo = create_foo();
  foo_add(foo, 3.14159);
  destroy_foo(foo);
}
6 Likes

Thanks for your reply! This type of discussion and explanation is exactly what I need.

Yes, that was exactly what I was trying to do. I thought the drop(Box::from_raw(self.ptr)) part was to needed to remove the memory from the heap (perhaps my head is in C land here), in my head I though that once the Rust object went out of scope drop would be called and I can take the actual data of the heap.

I actually did that first but I wanted to see if it was possible to kind of keep the Rust object as a container/wrapper for the memory and just let give out a C-pointer through the API. Is that possible? For curiosity how would a something like that look? Or if I wanted to make it a (shrug) singleton?

But you might be right in that I'm also breaking expectations on a C API.
Technically, I would even prefer no to give out anything and just do foo_add and have the whole memory piece be contained. But the caller obviously need a handle somehow to add to...

Other differences that I'm curious about.
You are returning -> *mut Foo in `

But I would actually like to return an bool/enum/result. That is why I did

unsafe {
    *out_ptr = handle;
}

is that the right way to do it?

Also what is the difference between doing

let _ = Box::from_raw(foo);

vs

drop(Box::from_raw(foo));

?

Thanks again!

One way would be to call the C code from Rust (but I doubt that's what you want).

You could leak the Foo so it never drops. You could put a pointer to the leaked value in a OnceCell.

You could have some potentially-non-leaking variation on this by using some global data store, but it would effectively leak unless the C code told you when it was ok to drop everything, so it's not really that different from dropping each object conceptually IMO.

1 Like

Nope, it's' not :slight_smile:

So many new things! I'll have to understand what this means and would look like :smile:

It depends on your function. If create_foo() is a simple constructor that can't fail then it makes sense to return a *mut Foo. If the failure mode is trivial or obvious, you could still use a *mut Foo pointer but have failures return a null pointer and document that people need to check the pointer before continuing.

Foo *create_foo();
void destroy_foo(Foo *foo);

void main() {
  Foo *foo = create_foo();
  if (foo) {
    // Do stuff with foo

    destroy_foo(foo);
  }
}

However, if creating a Foo is non-trivial and may fail in a number of ways, using out-pointers and returning a bool or Result enum may make sense, too.

#include <stdio.h>

typedef enum {
  STATUS_OK,
  STATUS_INVALID_ARGUMENT,
  STATUS_ACCESS_DENIED,
  // ...
} status_t;

const char* status_messages[] = {
  [STATUS_OK] = "Ok",
  [STATUS_INVALID_ARGUMENT] = "Invalid Argument",
  [STATUS_ACCESS_DENIED] = "Access Denied",
};

typedef struct Foo Foo;

status_t create_foo(Foo **foo);
void destroy_foo(Foo *foo);

int main() {
  Foo *foo;

  status_t status = create_foo(&foo);
  if (status != STATUS_OK) {
    printf("Error: %s\n", status_messages[status]);
    return 1;
  }

  // Do stuff with foo

  destroy_foo(foo);
}

TBH, I see Box::leak() as a code smell.

If you need to leak memory to achieve something, that tells me your program's ownership story is so poorly defined or convoluted that you just gave up instead of coming up with a proper design.

Yes, but the main question you need to think about is who owns that wrapper? If it is owned by some Rust code, you need to make sure the C code only ever borrows the object and control is returned to Rust so it gets a chance to clean it up (e.g. by letting the Rust invoke some sort of C callback with a &Foo reference).

You can always use a singleton that gets stored in some static variable in Rust land. That means you'll only need to provide a foo_add() function, but it also comes with all the down sides of a singleton (hard to test, implicit global state that can be modified from anywhere, resources never get freed, etc.). It's a valid approach, and one you see in C a fair amount of the time, but has a lot of down sides.

Otherwise, if the object is going to be owned and managed by C, you need to give C an explicit destructor it can call when it no longer needs the Foo. This is the approach I'd take by default unless there's a good reason not to.

The drop() function is nothing special. It's literally defined as

By assigning to _, I'm explicitly telling readers that we're taking the result of Box::from_raw() and throwing it away, which will make it go out of scope and invoke destructors.

For all intents and purposes, let _ = ... and drop(...) are equivalent.

4 Likes

Eh, it depends. For binaries, if you load data at the start and need it for the duration of the run, there's not necessarily any harm, and even an argument that it's more efficient for the OS to clean up POD resources. In contrast, for libraries, I'd almost always agree. But it can make sense for a singleton. Leaking a singleton is roughly equivalent to a lazy static.

Does a singleton make sense for the OP? I dunno, but they brought it up :slight_smile:.

2 Likes

@Michael-F-Bryan thanks! Really appreciate the detailed explanations I went with a simple

Foo *create_foo();
void foo_add(Foo *foo, float value);
void destroy_foo(Foo *foo);

for now.

Yes, I did! I was curious what it would look like in this context. No shame falls on anyone else but me :slight_smile:

1 Like

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.