| Crates.io | ffi_11 |
| lib.rs | ffi_11 |
| version | 0.0.2 |
| created_at | 2025-05-08 15:44:25.57371+00 |
| updated_at | 2025-05-16 11:31:30.806458+00 |
| description | One-to-one FFI types for C++/Rust interop. |
| homepage | |
| repository | https://github.com/google/crubit |
| max_upload_size | |
| id | 1665481 |
| size | 53,507 |
ffi_11: One-to-one FFI typesIn ffi_11, if a type is distinct in C/C++, it is distinct in Rust.
For example, char is a distinct type from signed char, even on platforms
where char is signed, and so ffi_11::c_char is always a distinct type from
ffi_11::c_schar.
For more details, see the API documentation.
ffi crate?The std::ffi module has a few shortcomings:
nullptr_t, (C++11, C23) or char8_t
(C++20).c_char are just aliases for other, existing types, it
presents a portability trap: even the documentation for functions
accepting a c_char will instead say i8 in public documentation
(example),
even though it's u8 on other platforms. There are no compiler errors or
lint warnings if you use i8 with APIs that actually accept c_char.Problem 3 can be solved with improvements to the documentation generator and
linters. These tools can use an approach like
Crubit's SugaredTy
to keep track of whether i8 came from the c_char alias, even though it is
not inherently part of the type system. It doesn't need a new ffi module!
Problem 2 is hopefully only a matter of adding some new aliases/types to the
ffi module. It, also, doesn't need a new ffi module.
Problem 1, however, is fundamental to the design of std::ffi.
The standard library ffi module defines multiple aliases to the same type,
where in C++ they would be different types. For example, Rust has two byte types
(i8, u8), but C++ has at least four (char, signed char, unsigned char,
and char8_t). This makes it fundamentally impossible to express certain C++
APIs as Rust APIs using the ffi module. Consider the following C++ constructor
overload set:
struct Foo {
Foo(char) {std::cout << "char\n";}
Foo(signed char) {std::cout << "signed char\n";}
Foo(unsigned char) {std::cout << "unsigned char\n";}
};
If we wanted to map these constructors to a From impl, there is no way to
write out all three! We might try the following:
impl From<ffi::c_char> for Foo {...}
impl From<ffi::c_schar> for Foo {...}
impl From<ffi::c_uchar> for Foo {...}
But on x86, c_char and c_schar are the same type, and so this is a
compilation error:
error[E0119]: conflicting implementations of trait `From<i8>` for type `Foo`
--> src/lib.rs:12:1
|
8 | impl From<ffi::c_char> for Foo {
| ------------------------------ first implementation here
...
12 | impl From<ffi::c_schar> for Foo {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Foo`
For more information about this error, try `rustc --explain E0119`.
This problem is not limited to bytes. Rust also has two 32-bit integer types. C++ has 4 on Linux, and 5 on Windows. And so on, with varying exact numbers, for all the sized types.
And this problem is not limited to obscure overload sets. If we want to support
templates, then what is to be done about std::vector<c_char> vs
std::vector<c_schar>? They cannot be the same type in Rust without UB, and
yet, since c_char and c_schar may be the same type, we end up in a bind:
// C++:
std::vector<char> Foo();
std::vector<signed char> Bar();
// Rust
pub fn Foo() -> std::vector<c_char>;
pub fn Bar() -> std::vector< ?? >;
Ideally, we would like a way to express distinct types in C++ as distinct types
in Rust, for the purpose of distinguishing them at compile time in trait lookup
and templates/generics. We want From<c_char> to be different from
From<c_schar>, and we want std::vector<c_char> to be a different type to
std::vector<c_schar>, because they are different in C++, and losing the
distinction in Rust would means not every C++ API is callable.
To achieve this, we created an alternate take on the ffi module: ffi_11.
The ffi_11 crate looks substantially identical to ffi from a user point of
view, and allows restating C++ APIs in Rust using its interop types. The
following C++ API and Rust API are equivalent, using ffi_11:
// C++
long Foo(signed char, char);
// Rust
pub fn Foo(_: ffi_11::c_schar, _: ffi_11::c_char) -> ffi_11::c_long;
// NOT EQUIVALENT TO `pub fn Foo(_: i8, _: i8) -> i64;` -- not on any platform.
Unlike ffi from the standard library, c_schar and c_char are guaranteed to
be different types.
ffi_11?The name is meant to allude to three things:
ffi, but one-to-one (and onto).ffi, but it also adds C++11 (and above) support. Many things that are
C++11-aware are more modern – this is also reminiscent of, for instance,
pybind11.