cbindgen icon indicating copy to clipboard operation
cbindgen copied to clipboard

() causes missing template arguments

Open jrmuizel opened this issue 5 years ago • 3 comments

#[repr(C)]
pub struct S<T, R> {
x: T,
y: R,
}

 #[no_mangle]
 pub extern "C" fn f() -> S<u32, ()> {
     panic!()
 }

generates

#include <cstdint>
#include <cstdlib>

template<typename T, typename R>
struct S {
  T x;
  R y;
};

extern "C" {

S<uint32_t> f();

} // extern "C"

which fails to compile with

out.cc:12:1: error: too few template arguments for class template 'S'
S<uint32_t> f();
^
out.cc:5:8: note: template is declared here
struct S {
       ^

jrmuizel avatar Oct 18 '18 02:10 jrmuizel

Hi, I have a solution to this, that from

#[repr("C")]
pub struct S<T> {
    x: T,
}

#[no_mangle]
pub extern "C" fn gives() -> *mut S<()> {
    loop {}
}

#[no_mangle]
pub extern "C" fn takes(x: *mut S<()>) {}

generates

template<typename T = void>
struct S;

extern "C" {

S<void> *gives();

void takes(S<void> *x);

I am wondering however if using S<void> is right here ? Are we sure that no weird void/() behavior might happen ? I would think this is ok, since we can only manipulate S through a pointer; but I would like a more educated opinion on this 😅

arnaudgolfouse avatar Jan 04 '21 22:01 arnaudgolfouse

Well i put some more thought into it, and I think there are 2 issues with this

  1. The original example would compile to
template<typename T, typename R>
struct S {
  T x;
  R y;
};

extern "C" {

S<uint32_t, void> f();

} // extern "C"

Which I believe to be unusable, because you can't create an object of type void. However this is already what c_void does, so why not. This does lead to the second issue 2. This conflicts with c_void ! So the following code

#[repr(C)]
pub struct S<T> {
    x: std::marker::PhantomData<T>,
}

#[no_mangle]
pub extern "C" fn create() -> *mut S<std::ffi::c_void> {
    loop {}
}

#[no_mangle]
pub extern "C" fn consume(_: *mut S<()>) {}

Produces

template<typename T>
struct S {};

extern "C" {

S<void> *create();

void consume(S<void>*);

} // extern "C"

And we can then write the incorrect code

void evil() {
    auto x = create();
    consume(x); // incorrect type !
}

But there are no other canonical sero-sized type in C++...

To conclude I don't think using () in cbindgen ever makes sense (except for the return type), and it should probably be an error to try to do so :upside_down_face:

arnaudgolfouse avatar Jan 07 '21 22:01 arnaudgolfouse

I think there is a way to properly accept (), as well as other zero-sized types like PhantomData inside the generated struct parameters. While C++ does not have a way to represent zero sized type, you can always omit the struct field when there is one. It can be done with partial specialization, like here:

template<typename T, typename C, typename R>
struct CGlueObjContainer {
    T instance;
    C context;
    R ret_tmp;
};

template<typename T, typename C>
struct CGlueObjContainer<T, C, void> {
    T instance;
    C context;
};

Then replace any () with void, instead of nothing, and have it magically disappear in the struct rather than parameter.

Now, to do this fully you have 2^N possible specializations to do for N typename parameters used as a value type in the struct, which is less than ideal. It could be mitigated by excluding "type parameters that will never be void" or doing it inclusively instead. In the end, it could solve this issue alongside https://github.com/eqrion/cbindgen/issues/99.

h33p avatar Oct 09 '21 16:10 h33p