-
Notifications
You must be signed in to change notification settings - Fork 382
Reusing Rust type bindings / support for type aliases in the Rust section #1539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
@@ -163,3 +163,46 @@ mod ffi { | |||
|
|||
Bounds on a lifetime (like `<'a, 'b: 'a>`) are not currently supported. Nor are | |||
type parameters or where-clauses. | |||
|
|||
## Reusing existing binding types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reusing an existing defined pair of C++ and Rust type is already supported. I don't understand the distinction between the extern C++ type aliases which are already supported, and the extern Rust type aliases in this PR. What is the situation where it would be correct to write extern "Rust" { type T = path::to::U; }
and not extern "C++" { type T = path::to::U; }
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reusing an existing defined pair of C++ and Rust type is already supported. I don't understand the distinction between the extern C++ type aliases which are already supported, and the extern Rust type aliases in this PR. What is the situation where it would be correct to write
extern "Rust" { type T = path::to::U; }
and notextern "C++" { type T = path::to::U; }
?
In the PR, in the end-to-end test under tests/ffi/...
the CrossModuleRustType
is a Rust type, rather than a C++ type. This is why extern "C++" { type T = path::to::U; }
will not work and we need to support extern "Rust" { type T = path::to::U; }
.
Let me consider different scenarios:
- If
Foo
is a C++ type (declared in another#[cxx::bridge]
'sextern "C++"
section) thenextern "C++" { type Foo = crate::some::other::module::Foo; }
will indeed work. - If
Foo
is a Rust type (declared in another Rust module, and also covered by another#[cxx::bridge]
) then:extern "C++" { type Foo = crate::some::other::module::Foo; }
will not work, becauseFoo
is a Rust type rather than a C++ type/binding. In particular, if I changetests/ffi/lib.rs
in the PR to put the type alias in anextern "C++"
section, thencargo test
rightfully fails saying: "the trait boundmodule::CrossModuleRustType: ExternType
is not satisfied".- I can't just say
extern "Rust" { type Foo; /* ... */ }
and havetype Foo = crate::some::other::module::Foo
outside ofcxx::bridge
, because this will error out with: "conflicting implementations of traitRustType
for typemodule::CrossModuleRustType
" (orFoo
, this error message was captured by tweaking the end-to-end tests from the PR).
verify | ||
verify | ||
} | ||
Lang::Rust => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not sufficient for safety. Consider adding:
// inside extern "Rust"
type Any = crate::module::CrossModuleRustType;
fn repro(bad: &mut CrossModuleRustType) -> &mut Any;
...
fn repro(bad: &mut ffi::CrossModuleRustType) -> &mut ffi::Any {
bad
}
Nothing here enforces what type the C++ Any
is. We can make it anything:
namespace tests {
using Any = std::array<char, 1000>;
}
and arbitrarily stomp on memory.
repro(*r_boxed_cross_module_rust_type(123)).fill('?');
Running tests/test.rs (target/debug/deps/test-f2f5723622a08a56)
running 15 tests
free(): invalid next size (fast)
malloc(): corrupted top size
error: test failed, to rerun pass `--test test`
Caused by:
process didn't exit successfully: `target/debug/deps/test-f2f5723622a08a56` (signal: 6, SIGABRT: process abort signal)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you say that Any
is a C++ type. It is a Rust type, right? (by virtue of ::cxx::private::verify_rust_type
which ensures that T: RustType
which means the aliased type must have been used in another cxx::bridge
's extern "Rust"
section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In particular, if I change the new tests/ffi/lib.rs
snippet from the PR to:
extern "Rust" {
type CrossModuleRustType = crate::ffi::C; // `C` is a C++ type - it won't work here.
fn r_get_value_from_cross_module_rust_type(
value: &CrossModuleRustType,
) -> i32;
}
then cargo test
expectedly reports an error that says this is disallowed: "the trait bound ffi::C: RustType
is not satisfied".
Maybe I just have trouble figuring out what scenario you have in mind. If the error above (i.e. "free(): invalid next size" and "malloc(): corrupted top size") can be reproed, then I would really appreciate if you could share the repro as a commit on top/after my PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the error above (i.e. "free(): invalid next size" and "malloc(): corrupted top size") can be reproed, then I would really appreciate if you could share the repro as a commit on top/after my PR?
This is what I mean: anforowicz#3. This would need to not compile in order for the implementation to be sound.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! That helped. I understand the problem now.
The specific scenario that you've pointed out should now be fixed in the latest version of the PR. The fix is to check on C++ side that the type alias is derived from ::rust::Opaque
. OTOH, the fix doesn't seem sufficient to solve the general problem - transmuting into an unrelated type seems still posslble with something like:
#[cxx::bridge]
mod ffi {
extern "Rust" {
type RustType;
type OtherRustType;
#[cxx_name = "OtherRustType"]
type RustAlias = RustType;
fn bad_transmute(x: &RustType) -> &RustAlias;
fn do_something_with_other_rust_type(x: &OtherRustType);
}
}
To prevent the above, we'd need to assert on C++ side not only that RustAlias
aliases an opaque Rust type, but that it aliases the same, specific Rust type. In other words, this probably requires a C++ equivalent of ExternType::Id
checks from Rust side.
Q1: Does the above sound more or less correct to you?
Q2: Do you have suggestions on how to implement the C++ equivalent of ExternType::Id
checks? Some initial thoughts below:
- It seems that the type id of a Rust type should include the type name, the module path, and the crate name. The latter seems difficult, because 1) IIUC
cxxbridge
does not currently know/get the crate name, and 2) crate names can be aliased. - One mechanism to add a mechanism for type id checks would be to tweak
write_opaque_type
so that the generated struct has an extra field like so:
#include <cstring>
struct S /* : ::rust::Opaque */ {
static constexpr const char* __cxxbridge1_rust_type_id = "module::path::RustType";
};
static_assert(
0 == strcmp(S::__cxxbridge1_rust_type_id, "module::path::OtherRustType"),
"blah"
);
(having a new public field is a bit icky but is not the end of the world; one alternative would be to introduce template parameters into ::rust::Opaque
, but changing this type should probably be treated as a breaking change which seems to make this approach undesirable)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still feel that all the use cases of this functionality are already well supported, with all the static checking required for soundness, by extern "C++"
. An extern "C++"
type alias means "I already have a C++ definition of a type (the left-hand side) and a Rust definition of a type (right-hand side) that refer to the same type". This is verified in Rust via ExternType::Id and redoing an equivalent check on the C++ side is unnecessary.
If I wipe out all the changes in {gen,macro,src,syntax} from this PR, but keep the changes in tests/, with this conversion to extern "C++"
the new tests pass:
--- a/tests/ffi/lib.rs
+++ b/tests/ffi/lib.rs
@@ -370,8 +370,11 @@ pub mod ffi {
impl SharedPtr<Private> {}
impl UniquePtr<Array> {}
- extern "Rust" {
+ extern "C++" {
type CrossModuleRustType = crate::module::CrossModuleRustType;
+ }
+
+ extern "Rust" {
fn r_get_value_from_cross_module_rust_type(value: &CrossModuleRustType) -> i32;
}
}
--- a/tests/ffi/module.rs
+++ b/tests/ffi/module.rs
@@ -82,6 +82,7 @@ pub mod ffi2 {
#[cxx::bridge(namespace = "tests")]
pub mod ffi3 {
extern "Rust" {
+ #[derive(ExternType)]
type CrossModuleRustType;
#[allow(clippy::unnecessary_box_returns)]
In 2.x it would be a good idea to provide ExternType impls for every opaque Rust type automatically, but this is a breaking change because they conflict with handwritten impls which may currently exist.
Other than that I remain unsure what is left to implement. Would it be less confusing if type aliases are placed at the module level outside of extern
? Or if RustType is changed to carry an Id associated type (and potentially made public)?
`syntax::parse::parse_items` is currently called from two places: from `macro/src/expand.rs` and from `gen/src/mod.rs`. (In the future it may be also called from a `syntax/test_support.rs` helper.) Before this commit, all those places had to destructure/interpret a `syntax::file::Module` into `content`, `namespace`, and `trusted`. After this commit, this destructuring/interpretation is deduplicated into `syntax::parse::parse_items`. This requires some minor gymnastics: * `gen/src/mod.rs` has to call `std::mem::take(&mut bridge.attrs)` instead of doing a partial destructuring and passing `bridge.attrs` directly. This is an extra complexity, but we already do the same thing in `macro/src/expand.rs` before this commit, so hopefully this is ok. * `fn parse_items` takes `&mut Module` rather than `Module` and "consumes" / `std::mem::take`s `module.content`, because `macro/src/expand.rs` needs to retain ownership of other `Module` fields. This also seems like an unfortunate extra complexity, but (again) before this commit we already do this for `bridge.attrs` in `macro/src/expand.rs`.
Before this commit `fn parse_apis` in `syntax/test_support.rs` would ignore the namespace in `#[cxx::bridge(namespace = "ignored")]`. In other words, the new test added in `syntax/namespace.rs` would fail before this commit. This commit fixes this. At a high-level this commit: * Moves two pieces of code from `gen/src/file.rs` into `syntax/...` to make them reusable from `syntax/test_support.rs`: - `fn find_cxx_bridge_attr` (moved into `syntax/attrs.rs`) - `Namespace::parse_attr` (moved into `syntax/namespace.rs`) * Reuses these pieces of code from `syntax/test_support.rs` * Renames `Namespace::parse_bridge_attr_namespace` to `Namespace::parse_stream` so that all 3 parse methods are named after their input: `parse_attr`, `parse_stream`, `parse_meta`. * Adds a `syntax/`-level unit test that verifies that the namespace is indeed getting correctly parsed and propagated
8dbb7b2
to
ab7b1cd
Compare
@dtolnay (regarding #1539 (comment) so I don't intrude on that thread), the use case I have that doesn't work with existing cxx is this:
Note again that This results in a build error:
If I attempt to alias them with
So in response to "Other than that I remain unsure what is left to implement", do you have a suggestion on how to support this use case? Ideally, I wouldn't have to put all the (huge) cxx code from multiple files into one as a workaround. |
PTAL?
This PR fixes https://crbug.com/433254489 (/cc @ShabbyX as FYI - IIUC they've patched this PR onto their local machine and verified that it does indeed unblock them).
The first commit fixes some existing code duplication and prevents more code duplication around how
fn syntax::parse_items
consumes asyntax::Module
. I think this commit is okay and moves things in a reasonable direction, but I acknowledge that adding more partial consumption/destructuring/taking is a bit icky. More discussion about this aspect of the changes can be found in a comment from a semi-internal review at anforowicz#2 (comment) and anforowicz#2 (comment).I also note that some existing variants of
syntax::Api
enum
have Rust and C++ versions - e.g.RustFunction
andCxxFunction
as well asRustType
andCxxType
. We could also do that for theTypeAlias
variant which this PR doesn't touch just yet. Please provide feedback on whether I should also submit an additional commit into this or a separate PR - see https://github.com/anforowicz/cxx/tree/api-rust-type-alias-separate-variant/cc @zetafunction who has kindly provided initial, semi-internal feedback at anforowicz#2