What's the right way to export a C API from a crate?

As kind of a followup to What’s the convention for handling a hybrid library and binary crate’s dependencies , I want to also have a C API for my crate to be exported as both a cdylib and staticlib. But I'm not sure what's the convention to do so ATM due to Cargo and rustc limitations.

You would expect it to be straightforward, but like the previous topic, it's not quite there yet. In particular:

Will make the resulting libraries bloat. And also will likely include the C API code in the Rust library and binary where it's not needed.

So is there a convention to do so cleanly? Any example projects?

The project in question where I'm considering adding this is this keepawake-rs.

Cargo is not good at building dynamic libraries. Besides this bug, it also can't control soname or rpath.

A good solution is to NOT set cdylib crate-type, and instead use cargo cbuild to build it properly:

2 Likes

Now I got stuck on something else:

error[E0507]: cannot move out of `*builder` which is behind a raw pointer
  --> src\capi.rs:31:16
   |
31 |     *builder = (*builder).display(display)
   |                ^^^^^^^^^^ ---------------- `*builder` moved due to this method call
   |                |
   |                move occurs because `*builder` has type `Builder`, which does not implement the `Copy` trait
   |
note: `Builder::display` takes ownership of the receiver `self`, which moves `*builder`
  --> src\lib.rs:62:24
   |
62 |     pub fn display(mut self, display: bool) -> Self {
   |                        ^^^^

The crate provides a consuming builder, which I tried to wrap behind a pointer to fit more into C world. But I can't seem to figure out what's the correct way to use such a consuming method behind such a pointer. I can convert to Box and back, but is that the right way? As I will have to remember to convert back to Box or it will drop...

You can move out from behind indirection using ptr::read(). Beware of panics and double-drops, though (it's hard to get this right).

1 Like

Only if the builder pointer has been created in Rust from a Box, then you could convert it back to Box. It's a cheap operation, so it's generally fine to do it when it helps.

Box will free the Builder in case something in Rust panics in its scope. In this case this may be okay, since extern "C" will not allow a panic to go through, so nothing else will have a chance to cause a double free. But if it was extern "C-unwind" fn or a Rust function, then you would have to worry if anything could survive the panic and then try to free the Builder for a second time.

If the builder pointer is not guaranteed come from a Box, e.g. it was cast from &mut Builder, then it's not safe to use Box::from_raw on it.

You should also be explicit whether this function allows null and uninitialized values or not. If the pointer must always be valid, then you can use builder: &mut Builder even in an extern "C" function. Then you would need some wrangling with std::mem::replace if you still needed to take ownership.

There is also a safe way to move temporarily out of a &mut:

2 Likes

I know this isn't the original post topic, but since we're talking about it...

safer-ffi solves a lot of the kinds of problems in the "something else" follow up.

1 Like

So I end up with something like this:

ptr::write(builder, ptr::read(builder).display(display));

To circumvent the fact that Rust doesn't know that this is to be considered an "owned" pointer, created from a Box, so should be safe to mutate via taking ownership and back. (AKA I think this is the Box special deref move semantics). Not the most agronomic syntax... And I wonder if the compiler is able to optimize something like this though... So it only writes the changed field rather than copying the entire struct back and forth...

That will double-drop (==UB) if .display(...) panics.

1 Like

What's the right way to do this then?

display just sets a field and does nothing else, so it should never panic. Alternatively you could use std::mem::replace to temporarily take ownership of the Builder, leaving behind a default instance. Something like this:

let mut builder_data = std::mem::replace(&mut *builder, Builder::new());
ptr::write(builder, builder_data.display(display));

In general the pattern of "mapping" a value by taking ownership and getting back ownership of a new value is not supported when you only have a mutable reference or equivalent to the data. The two options you have in these cases are to temporarily replace the data behind the mutable reference (like I just showed you) or take ownership anyway and abort if a panic happens before ownership of the data is put back (this is what the take_mut and replace_with crates do).

3 Likes

But doing such a replace will result in memcpy-ing the entire struct back and forth, which seems really inefficient... No?

If I understand things correctly, doing Box::from_raw and back into Box::into_raw will have the same issue with panics as well...

Is there really no simple way to wrap such a consuming builder conveniently and safely for FFI...

The way the display function is defined requires the struct to possibly be moved on the stack, there's no other way around it. Of course the compiler could optimize it (I would say this is likely given the simplicity of the function)

2 Likes

It does of course (In a release build), which is what makes using such ownership transfers in Rust still efficient and why I have often seen builders use mut self receivers like this.

But I want it to remain efficient when used via FFI, and I'm not sure what to do about this so the code remains idiomatic, ergonomic, correct, etc.

בתאריך יום ב׳, 6 בנוב׳ 2023, 19:38, מאת Ski Fire13 via The Rust Programming Language Forum ‏<notifications@rust-lang.discoursemail.com>:

Optimizations are not disabled your function is used across FFI.

If that still doesn't convince you then the only option is to change the Builder type and add a function that takes &mut self and sets the given value.

1 Like

In FFI with a dynamic library I don't think such optimizations can work? Or can they? Can Rust reason about a replace and not do a full memcpy... Curious, will have to try.

בתאריך יום ב׳, 6 בנוב׳ 2023, 20:00, מאת Ski Fire13 via The Rust Programming Language Forum ‏<notifications@rust-lang.discoursemail.com>:

It depends, what happens on the Rust side and what on the other side? My understanding is that Rust receives a pointer to the Builder and needs to "map" it through display, in which case it has all the informations to optimize that call.

Yeah, but it depends ptr::replace doesn't do anything that forces the memory copy or is done in a way that the compiler can't optimize (e.g. A library call, rather than some intrinsic). But it looks like it can:

ptr::replace(builder, ptr::replace(builder, Builder::new()).display(display));

Results in:

keepawake_set_display:
  0000000180005BE0: 88 51 48           mov         byte ptr [rcx+48h],dl
  0000000180005BE3: C3                 ret

But will likely result in code that does the actual replaces at runtime if the optimizations fail to reduce to this, or can't. e.g. Panicking code.

Still this is really not obvious and not very ergonomic... And I'd rather have the code be clean as well...

I can turn the Builder to use &mut self, but then create will need to clone it to have an owned copy, or still take ownership, which then means you won't be able to chain it off the config methods as they will return &mut Self... I wonder what's the right thing to do... I don't remember where I read or why I choose a consuming builder... Recommendations?

I meant to add the &mut self methods in addition to the current API

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.