mozilla / cbindgen

A project for generating C bindings from Rust code
Mozilla Public License 2.0
2.35k stars 303 forks source link

[Feature request] Introduce support for trait objects. #707

Open derchr opened 3 years ago

derchr commented 3 years ago

While it's already possible with some hacks, it would be nice to support them properly.

I understand that it is not possible to add support for fat pointers like &dyn Trait as it's ABI is not stable. But it should be possible to support pointers to fat pointers.

Code like this:

pub trait MyTrait {}

#[repr(C)]
pub struct MyStruct<T: std::fmt::Display> {
    data: T,
}

impl<T> MyTrait for MyStruct<T> where T: std::fmt::Display {}

#[no_mangle]
pub extern "C" fn MyStruct_create() -> *mut Box<dyn MyTrait> {
    Box::into_raw(Box::new(Box::new(MyStruct {
        data: 42,
    })))
}

Should generate a c-Header like this:

typedef struct MyTrait MyTrait;
MyTrait **MyStruct_create(void);

Currently this will not work for two reasons:

emilio commented 3 years ago

Seems reasonable to me. Part of the issue is that Box<dyn Trait> is not the same size as a real pointer, so you're double-boxing which seems unfortunate.

I think the hard part would be to know what to generate. MyTrait** isn't ideal, because it allows C to have a MyTrait* by dereferencing the pointer.

Something that should already work would be something like:

pub struct MyStruct(Box<dyn MyTrait>);

extern "C" fn MyStruct_Create(...) -> Box<MyStruct> { ... }

And should also be nicer to work with shouldn't it?

jcsahnwaldt commented 3 years ago

Although Rust's ABI for &dyn Trait isn't stable (yet?), CBindGen could allow users to generate C++ code for Rust traits if they activate a flag.

I put together a little example project showing how this could work. For this Rust code

pub trait Foo {
  extern "C" fn foo(&self);
}

#[no_mangle]
pub extern "C" fn get_foo() -> &'dyn Foo {
  // ...
}

we could generate this C++ header:

struct FooObj;

struct FooFns {
  VTable metadata;
  void (* const foo)(const FooObj *self);
};

struct FooDyn: Dyn<FooObj, FooFns> {
  void foo() const { fns->foo(self); }
};

FooDyn get_foo();

To keep the C++ code generated for a specific trait compact, I moved a bit of boilerplate shared by all such generated code to two helper classes. They could be in a C++ header file provided by CBindGen. (Of course, they could also be generated into each header file, if that's preferred.)

struct VTable {
  void (* const drop_in_place)(void*);
  const uintptr_t size_of;
  const uintptr_t align_of;
};

template<class Obj, class Fns>
struct Dyn {
  Obj* const self;
  Fns* const fns;
};

A bit of explanation

Rust's &dyn Trait values are fat pointers consisting of a pointer to the actual object (a struct) and a pointer to the vtable. The vtable in turn consist of a bit of metadata and the function pointers. The C++ classes shown above mirror these data structures. (The VTable struct above was actually generated by CBindGen from this Rust struct.)

The struct FooObj type is just a marker that adds a bit of type safety. (We could use void* instead if we want to generate less code.)

The list of typed function pointers in the vtable is generated into struct FooFns.

The method definitions in struct FooDyn are sweet syntactic sugar: we could omit them and use e.g. FooDyn dyn = get_foo(); dyn.fns->foo(dyn.self), but get_foo().foo() is much more readable.

Note that we don't use any virtual methods in C++. We don't want the C++ compiler to generate vtables – the Rust compiler already did that.

Possible problems

Of course, Rust may change its ABI any time (for example, they might add more metadata fields), but I think the basic structure will remain the same, and adapting CBindGen to such changes shouldn't be too much work.

Users will have to re-generate their C++ headers if they want to interface with a Rust version with a different ABI. I guess that would be acceptable for many users.

A bigger problem may be that some users may want to support multiple Rust versions with the same C++ code. I've got rough ideas how CBindGen could help with that, but nothing specific. Depends on how Rust changes its ABI.

Maybe the name of the flag that activates this feature should contain something scary like unstable. Or maybe telling users about the dangers is enough.

cztomsik commented 3 years ago

Also, it would be nice if we could somehow (with attr?) make cbindgen proceed anyway. For example I have a type:

#[repr(transparent)]
pub struct ObjId<T: ?Sized>(c_uint, PhantomData<T>);

which should be perfectly safe to use with dyn X because it's just an index. Right now I'm resorting to a hack described in https://github.com/eqrion/cbindgen/issues/385