commit 7db7369797538466d8abe3575010fafc13b8217b Author: David Tolnay Date: Sun Oct 20 14:51:12 2019 -0400 Safe FFI between Rust and C++ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a874449b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/Cargo.lock +/expand.cc +/expand.rs +/target diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..af5dce8e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: rust +rust: nightly +script: cargo run --manifest-path demo-rs/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..a6d34cc8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "cxx" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2018" +links = "cxxbridge00" +license = "MIT OR Apache-2.0" +description = "Safe interop between Rust and C++" +repository = "https://github.com/dtolnay/cxx" +documentation = "https://docs.rs/cxx" +readme = "README.md" + +[badges] +travis-ci = { repository = "dtolnay/cxx" } + +[dependencies] +anyhow = "1.0" +cc = "1.0.49" +codespan = "0.7" +codespan-reporting = "0.7" +cxxbridge-macro = { version = "0.0", path = "macro" } +proc-macro2 = { version = "1.0", features = ["span-locations"] } +quote = "1.0" +syn = { version = "1.0", features = ["full"] } +thiserror = "1.0" + +[build-dependencies] +cc = "1.0.49" + +[dev-dependencies] +rustversion = "1.0" +trybuild = "1.0" + +[workspace] +members = ["cmd", "demo-rs", "macro"] diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 00000000..75070770 --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1 @@ +github: dtolnay diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 00000000..16fe87b0 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..5ea03ee5 --- /dev/null +++ b/README.md @@ -0,0 +1,361 @@ +CXX — safe FFI between Rust and C++ +========================================= + +[![Build Status](https://api.travis-ci.com/dtolnay/cxx.svg?branch=master)](https://travis-ci.com/dtolnay/cxx) +[![Latest Version](https://img.shields.io/crates/v/cxx.svg)](https://crates.io/crates/cxx) +[![Rust Documentation](https://img.shields.io/badge/api-rustdoc-blue.svg)](https://docs.rs/cxx) + +This library provides a **safe** mechanism for calling C++ code from Rust and +Rust code from C++, not subject to the many ways that things can go wrong when +using bindgen or cbindgen to generate unsafe C-style bindings. + +```toml +[dependencies] +cxx = "0.0" +``` + +*Compiler support: requires rustc 1.42+ (beta on January 30, stable on March +12)* + +
+ +## Overview + +The idea is that we define the signatures of both sides of our FFI boundary +embedded together in one Rust module (the next section shows an example). From +this, CXX receives a complete picture of the boundary to perform static analyses +against the types and function signatures to uphold both Rust's and C++'s +invariants and requirements. + +If everything checks out statically, then CXX uses a pair of code generators to +emit the relevant `extern "C"` signatures on both sides together with any +necessary static assertions for later in the build process to verify +correctness. On the Rust side this code generator is simply an attribute +procedural macro. On the C++ side it can be a small Cargo build script if your +build is managed by Cargo, or for other build systems like Bazel or Buck we +provide a command line tool which generates the header and source file and +should be easy to integrate. + +The resulting FFI bridge operates at zero or negligible overhead, i.e. no +copying, no serialization, no memory allocation, no runtime checks needed. + +The FFI signatures are able to use native types from whichever side they please, +such as Rust's `String` or C++'s `std::string`, Rust's `Box` or C++'s +`std::unique_ptr`, Rust's `Vec` or C++'s `std::vector`, etc in any combination. +CXX guarantees an ABI-compatible signature that both sides understand, based on +builtin bindings for key standard library types to expose an idiomatic API on +those types to the other language. For example when manipulating a C++ string +from Rust, its `len()` method becomes a call of the `size()` member function +defined by C++; when manipulation a Rust string from C++, its `size()` member +function calls Rust's `len()`. + +
+ +## Example + +A runnable version of this example is provided under the *demo-rs* directory of +this repo (with the C++ side of the implementation in the *demo-cxx* directory). +To try it out, jump into demo-rs and run `cargo run`. + +```rust +#[cxx::bridge] +mod ffi { + // Any shared structs, whose fields will be visible to both languages. + struct SharedThing { + z: i32, + y: Box, + x: UniquePtr, + } + + extern "C" { + // One or more headers with the matching C++ declarations. Our code + // generators don't read it but it gets #include'd and used in static + // assertions to ensure our picture of the FFI boundary is accurate. + include!("demo-cxx/demo.h"); + + // Zero or more opaque types which both languages can pass around but + // only C++ can see the fields. + type ThingC; + + // Functions implemented in C++. + fn make_demo(appname: &str) -> UniquePtr; + fn get_name(thing: &ThingC) -> &CxxString; + fn do_thing(state: SharedThing); + } + + extern "Rust" { + // Zero or more opaque types which both languages can pass around but + // only Rust can see the fields. + type ThingR; + + // Functions implemented in Rust. + fn print_r(r: &ThingR); + } +} +``` + +Now we simply provide C++ definitions of all the things in the `extern "C"` +block and Rust definitions of all the things in the `extern "Rust"` block, and +get to call back and forth safely. + +Here are links to the complete set of source files involved in the demo: + +- [demo-rs/src/main.rs](demo-rs/src/main.rs) +- [demo-rs/build.rs](demo-rs/build.rs) +- [demo-cxx/demo.h](demo-cxx/demo.h) +- [demo-cxx/demo.cc](demo-cxx/demo.cc) + +To look at the code generated in both languages for the example by the CXX code +generators: + +```console + # run Rust code generator and print to stdout + # (requires https://github.com/dtolnay/cargo-expand) +$ cargo expand --manifest-path demo-rs/Cargo.toml + + # run C++ code generator and print to stdout +$ cargo run --manifest-path cmd/Cargo.toml -- demo-rs/src/main.rs +``` + +
+ +## Details + +As seen in the example, the language of the FFI boundary involves 3 kinds of +items: + +- **Shared structs** — their fields are made visible to both languages. + The definition written within cxx::bridge is the single source of truth. + +- **Opaque types** — their fields are secret from the other language. + These cannot be passed across the FFI by value but only behind an indirection, + such as a reference `&`, a Rust `Box`, or a `UniquePtr`. Can be a type alias + for an arbitrarily complicated generic language-specific type depending on + your use case. + +- **Functions** — implemented in either language, callable from the other + language. + +Within the `extern "C"` part of the CXX bridge we list the types and functions +for which C++ is the source of truth, as well as the header(s) that declare +those APIs. In the future it's possible that this section could be generated +bindgen-style from the headers but for now we need the signatures written out; +static assertions will verify that they are accurate. + +Within the `extern "Rust"` part, we list types and functions for which Rust is +the source of truth. These all implicitly refer to the `super` module, the +parent module of the CXX bridge. You can think of the two items listed in the +example above as being like `use super::ThingR` and `use super::print_r` except +re-exported to C++. The parent module will either contain the definitions +directly for simple things, or contain the relevant `use` statements to bring +them into scope from elsewhere. + +Your function implementations themselves, whether in C++ or Rust, *do not* need +to be defined as `extern "C"` ABI or no\_mangle. CXX will put in the right shims +where necessary to make it all work. + +
+ +## Comparison vs bindgen and cbindgen + +Notice that with CXX there is repetition of all the function signatures: they +are typed out once where the implementation is defined (in C++ or Rust) and +again inside the cxx::bridge module, though compile-time assertions guarantee +these are kept in sync. This is different from [bindgen] and [cbindgen] where +function signatures are typed by a human once and the tool consumes them in one +language and emits them in the other language. + +[bindgen]: https://github.com/rust-lang/rust-bindgen +[cbindgen]: https://github.com/eqrion/cbindgen/ + +This is because CXX fills a somewhat different role. It is a lower level tool +than bindgen or cbindgen in a sense; you can think of it as being a replacement +for the concept of `extern "C"` signatures as we know them, rather than a +replacement for a bindgen. It would be reasonable to build a higher level +bindgen-like tool on top of CXX which consumes a C++ header and/or Rust module +(and/or IDL like Thrift) as source of truth and generates the cxx::bridge, +eliminating the repetition while leveraging the static analysis safety +guarantees of CXX. + +But note in other ways CXX is higher level than the bindgens, with rich support +for common standard library types. Frequently with bindgen when we are dealing +with an idiomatic C++ API we would end up manually wrapping that API in C-style +raw pointer functions, applying bindgen to get unsafe raw pointer Rust +functions, and replicating the API again to expose those idiomatically in Rust. +That's a much worse form of repetition because it is unsafe all the way through. + +By using a CXX bridge as the shared understanding between the languages, rather +than `extern "C"` C-style signatures as the shared understanding, common FFI use +cases become expressible using 100% safe code. + +It would also be reasonable to mix and match, using CXX bridge for the 95% of +your FFI that is straightforward and doing the remaining few oddball signatures +the old fashioned way with bindgen and cbindgen, if for some reason CXX's static +restrictions get in the way. Please file an issue if you end up taking this +approach so that we know what ways it would be worthwhile to make the tool more +expressive. + +
+ +## Cargo-based setup + +For builds that are orchestrated by Cargo, you will use a build script that runs +CXX's C++ code generator and compiles the resulting C++ code along with any +other C++ code for your crate. + +The canonical build script is as follows. The indicated line returns a +[`cc::Build`] instance (from the usual widely used `cc` crate) on which you can +set up any additional source files and compiler flags as normal. + +[`cc::Build`]: https://docs.rs/cc/1.0/cc/struct.Build.html + +```rust +// build.rs + +fn main() { + cxx::Build::new() + .bridge("src/main.rs") // returns a cc::Build + .file("../demo-cxx/demo.cc") + .flag("-std=c++11") + .compile("cxxbridge-demo"); + + println!("cargo:rerun-if-changed=src/main.rs"); + println!("cargo:rerun-if-changed=../demo-cxx/demo.h"); + println!("cargo:rerun-if-changed=../demo-cxx/demo.cc"); +} +``` + +
+ +## Non-Cargo setup + +For use in non-Cargo builds like Bazel or Buck, CXX provides an alternate way of +invoking the C++ code generator as a standalone command line tool. The tool is +packaged as the `cxxbridge-cmd` crate on crates.io or can be built from the +*cmd* directory of this repo. + +```bash +$ cargo install cxxbridge-cmd + +$ cxxbridge src/main.rs --header > path/to/mybridge.h +$ cxxbridge src/main.rs > path/to/mybridge.cc +``` + +
+ +## Safety + +Be aware that the design of this library is intentionally restrictive and +opinionated! It isn't a goal to be powerful enough to handle arbitrary +signatures in either language. Instead this project is about carving out a +reasonably expressive set of functionality about which we can make useful safety +guarantees today and maybe extend over time. You may find that it takes some +practice to use CXX bridge effectively as it won't work in all the ways that you +are used to. + +Some of the considerations that go into ensuring safety are: + +- By design, our paired code generators work together to control both sides of + the FFI boundary. Ordinarily in Rust writing your own `extern "C"` blocks is + unsafe because the Rust compiler has no way to know whether the signatures + you've written actually match the signatures implemented in the other + language. With CXX we achieve that visibility and know what's on the other + side. + +- Our static analysis detects and prevents passing types by value that shouldn't + be passed by value from C++ to Rust, for example because they may contain + internal pointers that would be screwed up by Rust's move behavior. + +- To many people's surprise, it is possible to have a struct in Rust and a + struct in C++ with exactly the same layout / fields / alignment / everything, + and still not the same ABI when passed by value. This is a longstanding + bindgen bug that leads to segfaults in absolutely correct-looking code + ([rust-lang/rust-bindgen#778]). CXX knows about this and can insert the + necessary zero-cost workaround transparently where needed, so go ahead and + pass your structs by value without worries. This is made possible by owning + both sides of the boundary rather than just one. + +- Template instantiations: for example in order to expose a UniquePtr\ type + in Rust backed by a real C++ unique\_ptr, we have a way of using a Rust trait + to connect the behavior back to the template instantiations performed by the + other language. + +[rust-lang/rust-bindgen#778]: https://github.com/rust-lang/rust-bindgen/issues/778 + +
+ +## Builtin types + +In addition to all the primitive types (i32 ⟷ int32_t), the following common +types may be used in the fields of shared structs and the arguments and returns +of functions. + + + + + + + + + +
name in Rustname in C++restrictions
Stringcxxbridge::RustString
&strcxxbridge::RustStr
CxxStringstd::stringcannot be passed by value
Box<T>cxxbridge::RustBox<T>cannot hold opaque C++ type
UniquePtr<T>std::unique_ptr<T>cannot hold opaque Rust type
+ +The C++ API of the `cxxbridge` namespace is defined by the *include/cxxbridge.h* +file in this repo. You will need to include this header in your C++ code when +working with those types. + +The following types are intended to be supported "soon" but are just not +implemented yet. I don't expect any of these to be hard to make work but it's a +matter of designing a nice API for each in its non-native language. + + + + + + + + + + +
name in Rustname in C++
&[T]
Vec<T>
BTreeMap<K, V>
HashMap<K, V>
std::vector<T>
std::map<K, V>
std::unordered_map<K, V>
+ +
+ +## Remaining work + +This is still early days for CXX; I am releasing it as a minimum viable product +to collect feedback on the direction and invite collaborators. Here are some of +the facets that I still intend for this project to tackle: + +- [ ] Support associated methods: `extern "Rust" { fn f(self: &Struct); }` +- [ ] Support C++ member functions +- [ ] Support passing function pointers across the FFI +- [ ] Support translating between Result ⟷ exceptions +- [ ] Support structs with type parameters +- [ ] Support async functions + +On the build side, I don't have much experience with the `cc` crate so I expect +there may be someone who can suggest ways to make that aspect of this crate +friendlier or more robust. Please report issues if you run into trouble building +or linking any of this stuff. + +Finally, I know more about Rust library design than C++ library design so I +would appreciate help making the C++ APIs in this project more idiomatic where +anyone has suggestions. + +
+ +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be dual licensed as above, without any additional terms or conditions. + diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..4d8292e7 --- /dev/null +++ b/build.rs @@ -0,0 +1,9 @@ +fn main() { + cc::Build::new() + .file("src/cxxbridge.cc") + .flag("-std=c++11") + .compile("cxxbridge00"); + println!("cargo:rustc-flags=-l dylib=stdc++"); + println!("cargo:rerun-if-changed=src/cxxbridge.cc"); + println!("cargo:rerun-if-changed=include/cxxbridge.h"); +} diff --git a/cmd/Cargo.toml b/cmd/Cargo.toml new file mode 100644 index 00000000..b3f77194 --- /dev/null +++ b/cmd/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "cxxbridge-cmd" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "C++ code generator for integrating `cxx` crate into a non-Cargo build." +repository = "https://github.com/dtolnay/cxx" + +[[bin]] +name = "cxxbridge" +path = "src/main.rs" + +[badges] +travis-ci = { repository = "dtolnay/cxx" } + +[dependencies] +anyhow = "1.0" +codespan = "0.7" +codespan-reporting = "0.7" +proc-macro2 = { version = "1.0", features = ["span-locations"] } +quote = "1.0" +structopt = "0.3" +syn = { version = "1.0", features = ["full"] } +thiserror = "1.0" diff --git a/cmd/src/gen b/cmd/src/gen new file mode 120000 index 00000000..eb225777 --- /dev/null +++ b/cmd/src/gen @@ -0,0 +1 @@ +../../gen \ No newline at end of file diff --git a/cmd/src/main.rs b/cmd/src/main.rs new file mode 100644 index 00000000..daa5ac0d --- /dev/null +++ b/cmd/src/main.rs @@ -0,0 +1,29 @@ +mod gen; +mod syntax; + +use std::io::{self, Write}; +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +#[structopt(name = "cxxbridge", author)] +struct Opt { + /// Input Rust source file containing #[cxx::bridge] + #[structopt(parse(from_os_str))] + input: PathBuf, + + /// Emit header with declarations only + #[structopt(long)] + header: bool, +} + +fn main() { + let opt = Opt::from_args(); + let gen = if opt.header { + gen::do_generate_header + } else { + gen::do_generate_bridge + }; + let bridge = gen(&opt.input); + let _ = io::stdout().lock().write_all(bridge.as_ref()); +} diff --git a/cmd/src/syntax b/cmd/src/syntax new file mode 120000 index 00000000..83b00802 --- /dev/null +++ b/cmd/src/syntax @@ -0,0 +1 @@ +../../syntax \ No newline at end of file diff --git a/demo-cxx/demo.cc b/demo-cxx/demo.cc new file mode 100644 index 00000000..500ee3a7 --- /dev/null +++ b/demo-cxx/demo.cc @@ -0,0 +1,21 @@ +#include "demo-cxx/demo.h" +#include "demo-rs/src/main.rs" +#include + +namespace org { +namespace rust { + +ThingC::ThingC(std::string appname) : appname(std::move(appname)) {} + +ThingC::~ThingC() { std::cout << "done with ThingC" << std::endl; } + +std::unique_ptr make_demo(cxxbridge::RustStr appname) { + return std::unique_ptr(new ThingC(appname)); +} + +const std::string &get_name(const ThingC &thing) { return thing.appname; } + +void do_thing(SharedThing state) { print_r(*state.y); } + +} // namespace rust +} // namespace org diff --git a/demo-cxx/demo.h b/demo-cxx/demo.h new file mode 100644 index 00000000..51dd81bf --- /dev/null +++ b/demo-cxx/demo.h @@ -0,0 +1,24 @@ +#pragma once +#include "../include/cxxbridge.h" +#include +#include + +namespace org { +namespace rust { + +class ThingC { +public: + ThingC(std::string appname); + ~ThingC(); + + std::string appname; +}; + +struct SharedThing; + +std::unique_ptr make_demo(cxxbridge::RustStr appname); +const std::string &get_name(const ThingC &thing); +void do_thing(SharedThing state); + +} // namespace rust +} // namespace org diff --git a/demo-rs/Cargo.toml b/demo-rs/Cargo.toml new file mode 100644 index 00000000..f7e7f843 --- /dev/null +++ b/demo-rs/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cxxbridge-demo" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2018" +publish = false + +[dependencies] +cxx = { path = ".." } + +[build-dependencies] +cxx = { path = ".." } diff --git a/demo-rs/build.rs b/demo-rs/build.rs new file mode 100644 index 00000000..71def718 --- /dev/null +++ b/demo-rs/build.rs @@ -0,0 +1,11 @@ +fn main() { + cxx::Build::new() + .bridge("src/main.rs") + .file("../demo-cxx/demo.cc") + .flag("-std=c++11") + .compile("cxxbridge-demo"); + + println!("cargo:rerun-if-changed=src/main.rs"); + println!("cargo:rerun-if-changed=../demo-cxx/demo.h"); + println!("cargo:rerun-if-changed=../demo-cxx/demo.cc"); +} diff --git a/demo-rs/src/main.rs b/demo-rs/src/main.rs new file mode 100644 index 00000000..dba47274 --- /dev/null +++ b/demo-rs/src/main.rs @@ -0,0 +1,39 @@ +#[cxx::bridge(namespace = org::rust)] +mod ffi { + struct SharedThing { + z: i32, + y: Box, + x: UniquePtr, + } + + extern "C" { + include!("demo-cxx/demo.h"); + + type ThingC; + fn make_demo(appname: &str) -> UniquePtr; + fn get_name(thing: &ThingC) -> &CxxString; + fn do_thing(state: SharedThing); + } + + extern "Rust" { + type ThingR; + fn print_r(r: &ThingR); + } +} + +pub struct ThingR(usize); + +fn print_r(r: &ThingR) { + println!("called back with r={}", r.0); +} + +fn main() { + let x = ffi::make_demo("demo of cxx::bridge"); + println!("this is a {}", ffi::get_name(x.as_ref().unwrap())); + + ffi::do_thing(ffi::SharedThing { + z: 222, + y: Box::new(ThingR(333)), + x, + }); +} diff --git a/gen/error.rs b/gen/error.rs new file mode 100644 index 00000000..b8ac337f --- /dev/null +++ b/gen/error.rs @@ -0,0 +1,77 @@ +use crate::gen::Error; +use crate::syntax; +use anyhow::anyhow; +use codespan::{FileId, Files}; +use codespan_reporting::diagnostic::{Diagnostic, Label}; +use codespan_reporting::term::termcolor::{ColorChoice, StandardStream, WriteColor}; +use codespan_reporting::term::{self, Config}; +use std::io::Write; +use std::ops::Range; +use std::path::Path; +use std::process; + +pub(super) fn format_err(path: &Path, source: &str, error: Error) -> ! { + match error { + Error::Syn(syn_error) => { + let writer = StandardStream::stderr(ColorChoice::Auto); + let ref mut stderr = writer.lock(); + for error in syn_error { + let _ = writeln!(stderr); + display_syn_error(stderr, path, source, error); + } + } + _ => eprintln!("cxxbridge: {:?}", anyhow!(error)), + } + process::exit(1); +} + +fn display_syn_error(stderr: &mut dyn WriteColor, path: &Path, source: &str, error: syn::Error) { + let span = error.span(); + let start = span.start(); + let end = span.end(); + + let mut start_offset = 0; + for _ in 1..start.line { + start_offset += source[start_offset..].find('\n').unwrap() + 1; + } + start_offset += start.column; + + let mut end_offset = start_offset; + if start.line == end.line { + end_offset -= start.column; + } else { + for _ in 0..end.line - start.line { + end_offset += source[end_offset..].find('\n').unwrap() + 1; + } + } + end_offset += end.column; + + let mut files = Files::new(); + let file = files.add(path.to_string_lossy(), source); + + let range = start_offset as u32..end_offset as u32; + let diagnostic = diagnose(file, range, error); + + let config = Config::default(); + let _ = term::emit(stderr, &config, &files, &diagnostic); +} + +fn diagnose(file: FileId, range: Range, error: syn::Error) -> Diagnostic { + let message = error.to_string(); + let info = syntax::error::ERRORS + .iter() + .find(|e| message.contains(e.msg)); + let mut diagnostic = if let Some(info) = info { + let label = Label::new(file, range, info.label.unwrap_or(&message)); + let mut diagnostic = Diagnostic::new_error(&message, label); + if let Some(note) = info.note { + diagnostic = diagnostic.with_notes(vec![note.to_owned()]); + } + diagnostic + } else { + let label = Label::new(file, range, &message); + Diagnostic::new_error(&message, label) + }; + diagnostic.code = Some("cxxbridge".to_owned()); + diagnostic +} diff --git a/gen/include b/gen/include new file mode 120000 index 00000000..f5030fe8 --- /dev/null +++ b/gen/include @@ -0,0 +1 @@ +../include \ No newline at end of file diff --git a/gen/include.rs b/gen/include.rs new file mode 100644 index 00000000..00e67f03 --- /dev/null +++ b/gen/include.rs @@ -0,0 +1,13 @@ +static HEADER: &str = include_str!("include/cxxbridge.h"); + +pub fn get(guard: &str) -> &'static str { + let ifndef = format!("#ifndef {}\n", guard); + let endif = format!("#endif // {}\n", guard); + let begin = HEADER.find(&ifndef); + let end = HEADER.find(&endif); + if let (Some(begin), Some(end)) = (begin, end) { + &HEADER[begin..end + endif.len()] + } else { + panic!("not found in cxxbridge.h header: {}", guard) + } +} diff --git a/gen/mod.rs b/gen/mod.rs new file mode 100644 index 00000000..df9f3eee --- /dev/null +++ b/gen/mod.rs @@ -0,0 +1,114 @@ +// Functionality that is shared between the cxx::generate_bridge entry point and +// the cmd. + +mod error; +pub(super) mod include; +pub(super) mod out; +mod write; + +use self::error::format_err; +use self::out::OutFile; +use crate::syntax::{self, check, ident, Types}; +use quote::quote; +use std::fs; +use std::io; +use std::path::Path; +use syn::parse::ParseStream; +use syn::{Attribute, File, Item, Token}; +use thiserror::Error; + +pub(super) type Result = std::result::Result; + +#[derive(Error, Debug)] +pub(super) enum Error { + #[error("no #[cxx::bridge] module found")] + NoBridgeMod, + #[error("#[cxx::bridge] module must have inline contents")] + OutOfLineMod, + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Syn(#[from] syn::Error), +} + +struct Input { + namespace: Vec, + module: Vec, +} + +pub(super) fn do_generate_bridge(path: &Path) -> OutFile { + let header = false; + generate(path, header) +} + +pub(super) fn do_generate_header(path: &Path) -> OutFile { + let header = true; + generate(path, header) +} + +fn generate(path: &Path, header: bool) -> OutFile { + let source = match fs::read_to_string(path) { + Ok(source) => source, + Err(err) => format_err(path, "", Error::Io(err)), + }; + match (|| -> Result<_> { + let syntax = syn::parse_file(&source)?; + let bridge = find_bridge_mod(syntax)?; + let apis = syntax::parse_items(bridge.module)?; + let types = Types::collect(&apis)?; + check::typecheck(&apis, &types)?; + let out = write::gen(bridge.namespace, &apis, &types, header); + Ok(out) + })() { + Ok(out) => out, + Err(err) => format_err(path, &source, err), + } +} + +fn find_bridge_mod(syntax: File) -> Result { + for item in syntax.items { + if let Item::Mod(item) = item { + for attr in &item.attrs { + let path = &attr.path; + if quote!(#path).to_string() == "cxx :: bridge" { + let module = match item.content { + Some(module) => module.1, + None => { + return Err(Error::Syn(syn::Error::new_spanned( + item, + Error::OutOfLineMod, + ))); + } + }; + return Ok(Input { + namespace: parse_args(attr)?, + module, + }); + } + } + } + } + Err(Error::NoBridgeMod) +} + +fn parse_args(attr: &Attribute) -> syn::Result> { + if attr.tokens.is_empty() { + return Ok(Vec::new()); + } + attr.parse_args_with(|input: ParseStream| { + mod kw { + syn::custom_keyword!(namespace); + } + input.parse::()?; + input.parse::()?; + let path = syn::Path::parse_mod_style(input)?; + input.parse::>()?; + path.segments + .into_iter() + .map(|seg| { + ident::check(&seg.ident)?; + Ok(seg.ident.to_string()) + }) + .collect() + }) +} diff --git a/gen/out.rs b/gen/out.rs new file mode 100644 index 00000000..d6735be4 --- /dev/null +++ b/gen/out.rs @@ -0,0 +1,74 @@ +use std::fmt::{self, Arguments, Write}; + +pub(crate) struct OutFile { + pub namespace: Vec, + pub header: bool, + content: Vec, + section_pending: bool, + block: &'static str, + block_pending: bool, +} + +impl OutFile { + pub fn new(namespace: Vec, header: bool) -> Self { + OutFile { + namespace, + header, + content: Vec::new(), + section_pending: false, + block: "", + block_pending: false, + } + } + + // Write a blank line if the preceding section had any contents. + pub fn next_section(&mut self) { + self.section_pending = true; + } + + pub fn begin_block(&mut self, block: &'static str) { + self.block = block; + self.block_pending = true; + } + + pub fn end_block(&mut self) { + if self.block_pending { + self.block_pending = false; + } else { + self.content.extend_from_slice(b"} // "); + self.content.extend_from_slice(self.block.as_bytes()); + self.content.push(b'\n'); + self.block = ""; + self.section_pending = true; + } + } + + pub fn write_fmt(&mut self, args: Arguments) { + Write::write_fmt(self, args).unwrap(); + } +} + +impl Write for OutFile { + fn write_str(&mut self, s: &str) -> fmt::Result { + if !s.is_empty() { + if self.block_pending { + self.content.push(b'\n'); + self.content.extend_from_slice(self.block.as_bytes()); + self.content.extend_from_slice(b" {\n"); + self.block_pending = false; + self.section_pending = false; + } else if self.section_pending { + self.content.push(b'\n'); + self.section_pending = false; + } + self.content.extend_from_slice(s.as_bytes()); + } + Ok(()) + } +} + +impl AsRef<[u8]> for OutFile { + fn as_ref(&self) -> &[u8] { + &self.content + } +} diff --git a/gen/write.rs b/gen/write.rs new file mode 100644 index 00000000..fd92d501 --- /dev/null +++ b/gen/write.rs @@ -0,0 +1,623 @@ +use crate::gen::include; +use crate::gen::out::OutFile; +use crate::syntax::atom::Atom::{self, *}; +use crate::syntax::{Api, ExternFn, Struct, Type, Types, Var}; +use proc_macro2::Ident; + +pub(super) fn gen(namespace: Vec, apis: &[Api], types: &Types, header: bool) -> OutFile { + let mut out_file = OutFile::new(namespace.clone(), header); + let out = &mut out_file; + + if header { + writeln!(out, "#pragma once"); + } + + for api in apis { + if let Api::Include(include) = api { + writeln!(out, "#include \"{}\"", include.value().escape_default()); + } + } + + write_includes(out, types); + write_include_cxxbridge(out, types); + + if !header { + out.next_section(); + write_namespace_alias(out, types); + } + + out.next_section(); + for name in &namespace { + writeln!(out, "namespace {} {{", name); + } + + if header { + out.next_section(); + write_namespace_alias(out, types); + } + + out.next_section(); + for api in apis { + match api { + Api::Struct(strct) => write_struct_decl(out, &strct.ident), + Api::CxxType(ety) | Api::RustType(ety) => write_struct_decl(out, &ety.ident), + _ => {} + } + } + + for api in apis { + if let Api::Struct(strct) = api { + out.next_section(); + write_struct(out, strct); + } + } + + if !header { + out.begin_block("extern \"C\""); + for api in apis { + let (efn, write): (_, fn(_, _, _)) = match api { + Api::CxxFunction(efn) => (efn, write_cxx_function_shim), + Api::RustFunction(efn) => (efn, write_rust_function_decl), + _ => continue, + }; + out.next_section(); + write(out, efn, types); + } + out.end_block(); + } + + for api in apis { + if let Api::RustFunction(efn) = api { + out.next_section(); + write_rust_function_shim(out, efn, types); + } + } + + out.next_section(); + for name in namespace.iter().rev() { + writeln!(out, "}} // namespace {}", name); + } + + if !header { + out.next_section(); + write_generic_instantiations(out, types); + } + + out_file +} + +fn write_includes(out: &mut OutFile, types: &Types) { + let mut has_int = false; + let mut has_unique_ptr = false; + let mut has_string = false; + + for ty in types { + match ty { + Type::Ident(ident) => match Atom::from(ident) { + Some(U8) | Some(U16) | Some(U32) | Some(U64) | Some(Usize) | Some(I8) + | Some(I16) | Some(I32) | Some(I64) | Some(Isize) => has_int = true, + Some(CxxString) => has_string = true, + Some(Bool) | Some(RustString) | None => {} + }, + Type::UniquePtr(_) => has_unique_ptr = true, + _ => {} + } + } + + if has_int { + writeln!(out, "#include "); + } + if has_unique_ptr { + writeln!(out, "#include "); + } + if has_string { + writeln!(out, "#include "); + } +} + +fn write_include_cxxbridge(out: &mut OutFile, types: &Types) { + let mut needs_rust_box = false; + for ty in types { + if let Type::RustBox(_) = ty { + needs_rust_box = true; + break; + } + } + + out.begin_block("namespace cxxbridge00"); + if needs_rust_box { + writeln!(out, "// #include \"cxxbridge.h\""); + for line in include::get("CXXBRIDGE00_RUST_BOX").lines() { + if !line.trim_start().starts_with("//") { + writeln!(out, "{}", line); + } + } + } + out.end_block(); +} + +fn write_namespace_alias(out: &mut OutFile, types: &Types) { + let mut needs_namespace_alias = false; + for ty in types { + if let Type::RustBox(_) = ty { + needs_namespace_alias = true; + break; + } + } + + if needs_namespace_alias { + writeln!(out, "namespace cxxbridge = cxxbridge00;"); + } +} + +fn write_struct(out: &mut OutFile, strct: &Struct) { + for line in strct.doc.to_string().lines() { + writeln!(out, "//{}", line); + } + writeln!(out, "struct {} final {{", strct.ident); + for field in &strct.fields { + write!(out, " "); + write_type_space(out, &field.ty); + writeln!(out, "{};", field.ident); + } + writeln!(out, "}};"); +} + +fn write_struct_decl(out: &mut OutFile, ident: &Ident) { + writeln!(out, "struct {};", ident); +} + +fn write_cxx_function_shim(out: &mut OutFile, efn: &ExternFn, types: &Types) { + let indirect_return = efn + .ret + .as_ref() + .map_or(false, |ret| types.needs_indirect_abi(ret)); + write_extern_return_type(out, &efn.ret, types); + for name in out.namespace.clone() { + write!(out, "{}$", name); + } + write!(out, "cxxbridge00${}(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + write_extern_arg(out, arg, types); + } + if indirect_return { + if !efn.args.is_empty() { + write!(out, ", "); + } + write_return_type(out, &efn.ret); + write!(out, "*return$"); + } + writeln!(out, ") noexcept {{"); + write!(out, " "); + write_return_type(out, &efn.ret); + write!(out, "(*{}$)(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + write_type(out, &arg.ty); + } + writeln!(out, ") = {};", efn.ident); + write!(out, " "); + if indirect_return { + write!(out, "new (return$) "); + write_type(out, efn.ret.as_ref().unwrap()); + write!(out, "("); + } else if efn.ret.is_some() { + write!(out, "return "); + } + write!(out, "{}$(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + if let Type::RustBox(_) = &arg.ty { + write_type(out, &arg.ty); + write!(out, "::from_raw({})", arg.ident); + } else if let Type::UniquePtr(_) = &arg.ty { + write_type(out, &arg.ty); + write!(out, "({})", arg.ident); + } else if types.needs_indirect_abi(&arg.ty) { + write!(out, "std::move(*{})", arg.ident); + } else { + write!(out, "{}", arg.ident); + } + } + write!(out, ")"); + match &efn.ret { + Some(Type::RustBox(_)) => write!(out, ".into_raw()"), + Some(Type::UniquePtr(_)) => write!(out, ".release()"), + _ => {} + } + if indirect_return { + write!(out, ")"); + } + writeln!(out, ";"); + writeln!(out, "}}"); +} + +fn write_rust_function_decl(out: &mut OutFile, efn: &ExternFn, types: &Types) { + write_extern_return_type(out, &efn.ret, types); + for name in out.namespace.clone() { + write!(out, "{}$", name); + } + write!(out, "cxxbridge00${}(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + write_extern_arg(out, arg, types); + } + if efn + .ret + .as_ref() + .map_or(false, |ret| types.needs_indirect_abi(ret)) + { + if !efn.args.is_empty() { + write!(out, ", "); + } + write_return_type(out, &efn.ret); + write!(out, "*return$"); + } + writeln!(out, ") noexcept;"); +} + +fn write_rust_function_shim(out: &mut OutFile, efn: &ExternFn, types: &Types) { + let indirect_return = efn + .ret + .as_ref() + .map_or(false, |ret| types.needs_indirect_abi(ret)); + for line in efn.doc.to_string().lines() { + writeln!(out, "//{}", line); + } + write_return_type(out, &efn.ret); + write!(out, "{}(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + write_type_space(out, &arg.ty); + write!(out, "{}", arg.ident); + } + write!(out, ") noexcept"); + if out.header { + writeln!(out, ";"); + } else { + writeln!(out, " {{"); + write!(out, " "); + if indirect_return { + write!(out, "char return$[sizeof("); + write_type(out, efn.ret.as_ref().unwrap()); + writeln!(out, ")];"); + write!(out, " "); + } else if efn.ret.is_some() { + write!(out, "return "); + } + for name in out.namespace.clone() { + write!(out, "{}$", name); + } + write!(out, "cxxbridge00${}(", efn.ident); + for (i, arg) in efn.args.iter().enumerate() { + if i > 0 { + write!(out, ", "); + } + if types.needs_indirect_abi(&arg.ty) { + write!(out, "&"); + } + write!(out, "{}", arg.ident); + } + if indirect_return { + if !efn.args.is_empty() { + write!(out, ", "); + } + write!(out, "reinterpret_cast<"); + write_return_type(out, &efn.ret); + write!(out, "*>(return$)"); + } + writeln!(out, ");"); + if indirect_return { + write!(out, " return "); + write_type(out, efn.ret.as_ref().unwrap()); + write!(out, "(*reinterpret_cast<"); + write_return_type(out, &efn.ret); + writeln!(out, "*>(return$));"); + } + writeln!(out, "}}"); + } +} + +fn write_return_type(out: &mut OutFile, ty: &Option) { + match ty { + None => write!(out, "void "), + Some(ty) => write_type_space(out, ty), + } +} + +fn write_extern_return_type(out: &mut OutFile, ty: &Option, types: &Types) { + match ty { + Some(Type::RustBox(ty)) | Some(Type::UniquePtr(ty)) => { + write_type_space(out, &ty.inner); + write!(out, "*"); + } + Some(Type::Str(_)) => write!(out, "cxxbridge::RustStr::Repr "), + Some(ty) if types.needs_indirect_abi(ty) => write!(out, "void "), + _ => write_return_type(out, ty), + } +} + +fn write_extern_arg(out: &mut OutFile, arg: &Var, types: &Types) { + match &arg.ty { + Type::RustBox(ty) | Type::UniquePtr(ty) => { + write_type_space(out, &ty.inner); + write!(out, "*"); + } + Type::Str(_) => write!(out, "cxxbridge::RustStr::Repr "), + _ => write_type_space(out, &arg.ty), + } + if types.needs_indirect_abi(&arg.ty) { + write!(out, "*"); + } + write!(out, "{}", arg.ident); +} + +fn write_type(out: &mut OutFile, ty: &Type) { + match ty { + Type::Ident(ident) => match Atom::from(ident) { + Some(Bool) => write!(out, "bool"), + Some(U8) => write!(out, "uint8_t"), + Some(U16) => write!(out, "uint16_t"), + Some(U32) => write!(out, "uint32_t"), + Some(U64) => write!(out, "uint64_t"), + Some(Usize) => write!(out, "size_t"), + Some(I8) => write!(out, "int8_t"), + Some(I16) => write!(out, "int16_t"), + Some(I32) => write!(out, "int32_t"), + Some(I64) => write!(out, "int64_t"), + Some(Isize) => write!(out, "ssize_t"), + Some(CxxString) => write!(out, "std::string"), + Some(RustString) => write!(out, "cxxbridge::RustString"), + None => write!(out, "{}", ident), + }, + Type::RustBox(ty) => { + write!(out, "cxxbridge::RustBox<"); + write_type(out, &ty.inner); + write!(out, ">"); + } + Type::UniquePtr(ptr) => { + write!(out, "std::unique_ptr<"); + write_type(out, &ptr.inner); + write!(out, ">"); + } + Type::Ref(r) => { + if r.mutability.is_none() { + write!(out, "const "); + } + write_type(out, &r.inner); + write!(out, " &"); + } + Type::Str(_) => { + write!(out, "cxxbridge::RustStr"); + } + } +} + +fn write_type_space(out: &mut OutFile, ty: &Type) { + write_type(out, ty); + match ty { + Type::Ident(_) | Type::RustBox(_) | Type::UniquePtr(_) | Type::Str(_) => write!(out, " "), + Type::Ref(_) => {} + } +} + +fn write_generic_instantiations(out: &mut OutFile, types: &Types) { + fn allow_unique_ptr(ident: &Ident) -> bool { + Atom::from(ident).is_none() + } + + out.begin_block("extern \"C\""); + for ty in types { + if let Type::RustBox(ty) = ty { + if let Type::Ident(inner) = &ty.inner { + out.next_section(); + write_rust_box_extern(out, inner); + } + } else if let Type::UniquePtr(ptr) = ty { + if let Type::Ident(inner) = &ptr.inner { + if allow_unique_ptr(inner) { + out.next_section(); + write_unique_ptr(out, inner); + } + } + } + } + out.end_block(); + + out.begin_block("namespace cxxbridge00"); + for ty in types { + if let Type::RustBox(ty) = ty { + if let Type::Ident(inner) = &ty.inner { + write_rust_box_impl(out, inner); + } + } + } + out.end_block(); +} + +fn write_rust_box_extern(out: &mut OutFile, ident: &Ident) { + let mut inner = String::new(); + for name in &out.namespace { + inner += name; + inner += "::"; + } + inner += &ident.to_string(); + let instance = inner.replace("::", "$"); + + writeln!(out, "#ifndef CXXBRIDGE00_RUST_BOX_{}", instance); + writeln!(out, "#define CXXBRIDGE00_RUST_BOX_{}", instance); + writeln!( + out, + "void cxxbridge00$rust_box${}$uninit(cxxbridge::RustBox<{}> *ptr) noexcept;", + instance, inner, + ); + writeln!( + out, + "void cxxbridge00$rust_box${}$set_raw(cxxbridge::RustBox<{}> *ptr, {} *raw) noexcept;", + instance, inner, inner + ); + writeln!( + out, + "void cxxbridge00$rust_box${}$drop(cxxbridge::RustBox<{}> *ptr) noexcept;", + instance, inner, + ); + writeln!( + out, + "const {} *cxxbridge00$rust_box${}$deref(const cxxbridge::RustBox<{}> *ptr) noexcept;", + inner, instance, inner, + ); + writeln!( + out, + "{} *cxxbridge00$rust_box${}$deref_mut(cxxbridge::RustBox<{}> *ptr) noexcept;", + inner, instance, inner, + ); + writeln!(out, "#endif // CXXBRIDGE00_RUST_BOX_{}", instance); +} + +fn write_rust_box_impl(out: &mut OutFile, ident: &Ident) { + let mut inner = String::new(); + for name in &out.namespace { + inner += name; + inner += "::"; + } + inner += &ident.to_string(); + let instance = inner.replace("::", "$"); + + writeln!(out, "template <>"); + writeln!(out, "void RustBox<{}>::uninit() noexcept {{", inner); + writeln!( + out, + " return cxxbridge00$rust_box${}$uninit(this);", + instance + ); + writeln!(out, "}}"); + + writeln!(out, "template <>"); + writeln!( + out, + "void RustBox<{}>::set_raw({} *raw) noexcept {{", + inner, inner, + ); + writeln!( + out, + " return cxxbridge00$rust_box${}$set_raw(this, raw);", + instance + ); + writeln!(out, "}}"); + + writeln!(out, "template <>"); + writeln!(out, "void RustBox<{}>::drop() noexcept {{", inner); + writeln!( + out, + " return cxxbridge00$rust_box${}$drop(this);", + instance + ); + writeln!(out, "}}"); + + writeln!(out, "template <>"); + writeln!( + out, + "const {} *RustBox<{}>::deref() const noexcept {{", + inner, inner, + ); + writeln!( + out, + " return cxxbridge00$rust_box${}$deref(this);", + instance + ); + writeln!(out, "}}"); + + writeln!(out, "template <>"); + writeln!( + out, + "{} *RustBox<{}>::deref_mut() noexcept {{", + inner, inner, + ); + writeln!( + out, + " return cxxbridge00$rust_box${}$deref_mut(this);", + instance + ); + writeln!(out, "}}"); +} + +fn write_unique_ptr(out: &mut OutFile, ident: &Ident) { + let mut inner = String::new(); + for name in &out.namespace { + inner += name; + inner += "::"; + } + inner += &ident.to_string(); + let instance = inner.replace("::", "$"); + + writeln!(out, "#ifndef CXXBRIDGE00_UNIQUE_PTR_{}", instance); + writeln!(out, "#define CXXBRIDGE00_UNIQUE_PTR_{}", instance); + writeln!( + out, + "static_assert(sizeof(std::unique_ptr<{}>) == sizeof(void *), \"\");", + inner, + ); + writeln!( + out, + "static_assert(alignof(std::unique_ptr<{}>) == alignof(void *), \"\");", + inner, + ); + writeln!( + out, + "void cxxbridge00$unique_ptr${}$null(std::unique_ptr<{}> *ptr) noexcept {{", + instance, inner, + ); + writeln!(out, " new (ptr) std::unique_ptr<{}>();", inner); + writeln!(out, "}}"); + writeln!( + out, + "void cxxbridge00$unique_ptr${}$new(std::unique_ptr<{}> *ptr, {} *value) noexcept {{", + instance, inner, inner, + ); + writeln!( + out, + " new (ptr) std::unique_ptr<{}>(new {}(std::move(*value)));", + inner, inner, + ); + writeln!(out, "}}"); + writeln!( + out, + "void cxxbridge00$unique_ptr${}$raw(std::unique_ptr<{}> *ptr, {} *raw) noexcept {{", + instance, inner, inner, + ); + writeln!(out, " new (ptr) std::unique_ptr<{}>(raw);", inner); + writeln!(out, "}}"); + writeln!( + out, + "const {} *cxxbridge00$unique_ptr${}$get(const std::unique_ptr<{}>& ptr) noexcept {{", + inner, instance, inner, + ); + writeln!(out, " return ptr.get();"); + writeln!(out, "}}"); + writeln!( + out, + "{} *cxxbridge00$unique_ptr${}$release(std::unique_ptr<{}>& ptr) noexcept {{", + inner, instance, inner, + ); + writeln!(out, " return ptr.release();"); + writeln!(out, "}}"); + writeln!( + out, + "void cxxbridge00$unique_ptr${}$drop(std::unique_ptr<{}> *ptr) noexcept {{", + instance, inner, + ); + writeln!(out, " ptr->~unique_ptr();"); + writeln!(out, "}}"); + writeln!(out, "#endif // CXXBRIDGE00_UNIQUE_PTR_{}", instance); +} diff --git a/include/cxxbridge.h b/include/cxxbridge.h new file mode 100644 index 00000000..31350927 --- /dev/null +++ b/include/cxxbridge.h @@ -0,0 +1,132 @@ +#pragma once +#include +#include +#include +#include + +namespace cxxbridge00 { + +class RustString final { +public: + RustString() noexcept; + RustString(const RustString &other) noexcept; + RustString(RustString &&other) noexcept; + RustString(const char *s); + RustString(const std::string &s); + RustString &operator=(const RustString &other) noexcept; + RustString &operator=(RustString &&other) noexcept; + ~RustString() noexcept; + operator std::string() const; + + // Note: no null terminator. + const char *data() const noexcept; + size_t size() const noexcept; + size_t length() const noexcept; + +private: + // Size and alignment statically verified by rust_string.rs. + std::array repr; +}; + +class RustStr final { +public: + RustStr() noexcept; + RustStr(const char *s); + RustStr(const std::string &s); + RustStr(std::string &&s) = delete; + RustStr(const RustStr &other) noexcept; + RustStr &operator=(RustStr other) noexcept; + operator std::string() const; + + // Note: no null terminator. + const char *data() const noexcept; + size_t size() const noexcept; + size_t length() const noexcept; + + // Repr is PRIVATE; must not be used other than by our generated code. + // + // Not necessarily ABI compatible with &str. Codegen will translate to + // cxx::rust_str::RustStr which matches this layout. + struct Repr { + const char *ptr; + size_t len; + }; + RustStr(Repr repr) noexcept; + operator Repr() noexcept; + +private: + Repr repr; +}; + +#ifndef CXXBRIDGE00_RUST_BOX +#define CXXBRIDGE00_RUST_BOX +template class RustBox final { +public: + RustBox(const RustBox &other) : RustBox(*other) {} + RustBox(RustBox &&other) noexcept : repr(other.repr) { other.repr = 0; } + RustBox(const T &val) { + this->uninit(); + new (this->deref_mut()) T(val); + } + RustBox &operator=(const RustBox &other) { + if (this != &other) { + if (this->repr) { + **this = *other; + } else { + this->uninit(); + new (this->deref_mut()) T(*other); + } + } + return *this; + } + RustBox &operator=(RustBox &&other) noexcept { + if (this->repr) { + this->drop(); + } + this->repr = other.repr; + other.repr = 0; + return *this; + } + ~RustBox() noexcept { + if (this->repr) { + this->drop(); + } + } + + const T *operator->() const noexcept { return this->deref(); } + const T &operator*() const noexcept { return *this->deref(); } + T *operator->() noexcept { return this->deref_mut(); } + T &operator*() noexcept { return *this->deref_mut(); } + + // Important: requires that `raw` came from an into_raw call. Do not pass a + // pointer from `new` or any other source. + static RustBox from_raw(T *raw) noexcept { + RustBox box; + box.set_raw(raw); + return box; + } + + T *into_raw() noexcept { + T *raw = this->deref_mut(); + this->repr = 0; + return raw; + } + +private: + RustBox() noexcept {} + void uninit() noexcept; + void set_raw(T *) noexcept; + T *get_raw() noexcept; + void drop() noexcept; + const T *deref() const noexcept; + T *deref_mut() noexcept; + uintptr_t repr; +}; +#endif // CXXBRIDGE00_RUST_BOX + +std::ostream &operator<<(std::ostream &os, const RustString &s); +std::ostream &operator<<(std::ostream &os, const RustStr &s); + +} // namespace cxxbridge00 + +namespace cxxbridge = cxxbridge00; diff --git a/macro/Cargo.toml b/macro/Cargo.toml new file mode 100644 index 00000000..f7256e0e --- /dev/null +++ b/macro/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cxxbridge-macro" +version = "0.0.0" +authors = ["David Tolnay "] +edition = "2018" +license = "MIT OR Apache-2.0" +description = "Implementation detail of the `cxx` crate." +repository = "https://github.com/dtolnay/cxx" + +[lib] +proc-macro = true + +[badges] +travis-ci = { repository = "dtolnay/cxx" } + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full"] } + +[dev-dependencies] +cxx = { version = "0.0", path = ".." } diff --git a/macro/src/expand.rs b/macro/src/expand.rs new file mode 100644 index 00000000..63f70322 --- /dev/null +++ b/macro/src/expand.rs @@ -0,0 +1,447 @@ +use crate::namespace::Namespace; +use crate::syntax::atom::Atom; +use crate::syntax::{self, check, Api, ExternFn, ExternType, Struct, Type, Types, Var}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::{Error, ItemMod, Result, Token}; + +pub fn bridge(namespace: &Namespace, ffi: ItemMod) -> Result { + let ident = &ffi.ident; + let content = ffi.content.ok_or(Error::new( + Span::call_site(), + "#[cxx::bridge] module must have inline contents", + ))?; + let apis = syntax::parse_items(content.1)?; + let ref types = Types::collect(&apis)?; + check::typecheck(&apis, types)?; + + let mut expanded = TokenStream::new(); + let mut hidden = TokenStream::new(); + let mut has_rust_type = false; + + for api in &apis { + if let Api::RustType(ety) = api { + expanded.extend(expand_rust_type(ety)); + if !has_rust_type { + hidden.extend(quote!(const fn __assert_sized() {})); + has_rust_type = true; + } + let ident = &ety.ident; + hidden.extend(quote!(__assert_sized::<#ident>();)); + } + } + + for api in &apis { + match api { + Api::Include(_) | Api::RustType(_) => {} + Api::Struct(strct) => expanded.extend(expand_struct(strct)), + Api::CxxType(ety) => expanded.extend(expand_cxx_type(ety)), + Api::CxxFunction(efn) => { + expanded.extend(expand_cxx_function_shim(namespace, efn, types)); + } + Api::RustFunction(efn) => { + hidden.extend(expand_rust_function_shim(namespace, efn, types)) + } + } + } + + for ty in types { + if let Type::RustBox(ty) = ty { + if let Type::Ident(ident) = &ty.inner { + if Atom::from(ident).is_none() { + hidden.extend(expand_rust_box(namespace, ident)); + } + } + } else if let Type::UniquePtr(ptr) = ty { + if let Type::Ident(ident) = &ptr.inner { + if Atom::from(ident).is_none() { + expanded.extend(expand_unique_ptr(namespace, ident)); + } + } + } + } + + // Work around https://github.com/rust-lang/rust/issues/67851. + if !hidden.is_empty() { + expanded.extend(quote! { + #[doc(hidden)] + const _: () = { + #hidden + }; + }); + } + + let attrs = ffi + .attrs + .into_iter() + .filter(|attr| attr.path.is_ident("doc")); + let vis = &ffi.vis; + + Ok(quote! { + #(#attrs)* + #[deny(improper_ctypes)] + #[allow(non_snake_case)] + #vis mod #ident { + #expanded + } + }) +} + +fn expand_struct(strct: &Struct) -> TokenStream { + let ident = &strct.ident; + let doc = &strct.doc; + let derives = &strct.derives; + let fields = strct.fields.iter().map(|field| { + // This span on the pub makes "private type in public interface" errors + // appear in the right place. + let vis = Token![pub](field.ident.span()); + quote!(#vis #field) + }); + quote! { + #doc + #[derive(#(#derives),*)] + #[repr(C)] + pub struct #ident { + #(#fields,)* + } + } +} + +fn expand_cxx_type(ety: &ExternType) -> TokenStream { + let ident = &ety.ident; + let doc = &ety.doc; + quote! { + #doc + #[repr(C)] + pub struct #ident { + _private: ::cxx::private::Opaque, + } + } +} + +fn expand_cxx_function_decl(namespace: &Namespace, efn: &ExternFn, types: &Types) -> TokenStream { + let ident = &efn.ident; + let args = efn.args.iter().map(|arg| expand_extern_arg(arg, types)); + let ret = expand_extern_return_type(&efn.ret, types); + let mut outparam = None; + if indirect_return(&efn.ret, types) { + let ret = expand_extern_type(efn.ret.as_ref().unwrap()); + outparam = Some(quote!(__return: *mut #ret)); + } + let link_name = format!("{}cxxbridge00${}", namespace, ident); + let local_name = format_ident!("__{}", ident); + quote! { + #[link_name = #link_name] + fn #local_name(#(#args,)* #outparam) #ret; + } +} + +fn expand_cxx_function_shim(namespace: &Namespace, efn: &ExternFn, types: &Types) -> TokenStream { + let ident = &efn.ident; + let doc = &efn.doc; + let decl = expand_cxx_function_decl(namespace, efn, types); + let args = &efn.args; + let ret = expand_return_type(&efn.ret); + let indirect_return = indirect_return(&efn.ret, types); + let vars = efn.args.iter().map(|arg| { + let var = &arg.ident; + match &arg.ty { + Type::Ident(ident) if ident == "String" => { + quote!(#var.as_mut_ptr() as *mut ::cxx::private::RustString) + } + Type::RustBox(_) => quote!(::std::boxed::Box::into_raw(#var)), + Type::UniquePtr(_) => quote!(::cxx::UniquePtr::into_raw(#var)), + Type::Ref(ty) => match &ty.inner { + Type::Ident(ident) if ident == "String" => { + quote!(::cxx::private::RustString::from_ref(#var)) + } + _ => quote!(#var), + }, + Type::Str(_) => quote!(::cxx::private::RustStr::from(#var)), + ty if types.needs_indirect_abi(ty) => quote!(#var.as_mut_ptr()), + _ => quote!(#var), + } + }); + let mut setup = efn + .args + .iter() + .filter(|arg| types.needs_indirect_abi(&arg.ty)) + .map(|arg| { + let var = &arg.ident; + // These are arguments for which C++ has taken ownership of the data + // behind the mut reference it received. + quote! { + let mut #var = std::mem::MaybeUninit::new(#var); + } + }) + .collect::(); + let local_name = format_ident!("__{}", ident); + let call = if indirect_return { + let ret = expand_extern_type(efn.ret.as_ref().unwrap()); + setup.extend(quote! { + let mut __return = ::std::mem::MaybeUninit::<#ret>::uninit(); + #local_name(#(#vars,)* __return.as_mut_ptr()); + }); + quote! { + __return.assume_init() + } + } else { + quote! { + #local_name(#(#vars),*) + } + }; + let expr = efn + .ret + .as_ref() + .and_then(|ret| match ret { + Type::Ident(ident) if ident == "String" => Some(quote!(#call.into_string())), + Type::RustBox(_) => Some(quote!(::std::boxed::Box::from_raw(#call))), + Type::UniquePtr(_) => Some(quote!(::cxx::UniquePtr::from_raw(#call))), + Type::Ref(ty) => match &ty.inner { + Type::Ident(ident) if ident == "String" => Some(quote!(#call.as_string())), + _ => None, + }, + Type::Str(_) => Some(quote!(#call.as_str())), + _ => None, + }) + .unwrap_or(call); + quote! { + #doc + pub fn #ident(#(#args),*) #ret { + extern "C" { + #decl + } + unsafe { + #setup + #expr + } + } + } +} + +fn expand_rust_type(ety: &ExternType) -> TokenStream { + let ident = &ety.ident; + quote! { + use super::#ident; + } +} + +fn expand_rust_function_shim(namespace: &Namespace, efn: &ExternFn, types: &Types) -> TokenStream { + let ident = &efn.ident; + let args = efn.args.iter().map(|arg| expand_extern_arg(arg, types)); + let vars = efn.args.iter().map(|arg| { + let ident = &arg.ident; + if types.needs_indirect_abi(&arg.ty) { + quote!(::std::ptr::read(#ident)) + } else { + quote!(#ident) + } + }); + let mut outparam = None; + let call = quote! { + ::cxx::private::catch_unwind(__fn, move || super::#ident(#(#vars),*)) + }; + let mut expr = efn + .ret + .as_ref() + .and_then(|ret| match ret { + Type::Ident(ident) if ident == "String" => { + Some(quote!(::cxx::private::RustString::from(#call))) + } + Type::RustBox(_) => Some(quote!(::std::boxed::Box::into_raw(#call))), + Type::UniquePtr(_) => Some(quote!(::cxx::UniquePtr::into_raw(#call))), + Type::Ref(ty) => match &ty.inner { + Type::Ident(ident) if ident == "String" => { + Some(quote!(::cxx::private::RustString::from_ref(#call))) + } + _ => None, + }, + Type::Str(_) => Some(quote!(::cxx::private::RustStr::from(#call))), + _ => None, + }) + .unwrap_or(call); + if indirect_return(&efn.ret, types) { + let ret = expand_extern_type(efn.ret.as_ref().unwrap()); + outparam = Some(quote!(__return: *mut #ret)); + expr = quote!(::std::ptr::write(__return, #expr)); + } + let ret = expand_extern_return_type(&efn.ret, types); + let link_name = format!("{}cxxbridge00${}", namespace, ident); + let local_name = format_ident!("__{}", ident); + let catch_unwind_label = format!("::{}", ident); + quote! { + #[doc(hidden)] + #[export_name = #link_name] + unsafe extern "C" fn #local_name(#(#args,)* #outparam) #ret { + let __fn = concat!(module_path!(), #catch_unwind_label); + #expr + } + } +} + +fn expand_rust_box(namespace: &Namespace, ident: &Ident) -> TokenStream { + let link_prefix = format!("cxxbridge00$rust_box${}{}$", namespace, ident); + let link_uninit = format!("{}uninit", link_prefix); + let link_set_raw = format!("{}set_raw", link_prefix); + let link_drop = format!("{}drop", link_prefix); + let link_deref = format!("{}deref", link_prefix); + let link_deref_mut = format!("{}deref_mut", link_prefix); + + let local_prefix = format_ident!("{}__box_", ident); + let local_uninit = format_ident!("{}uninit", local_prefix); + let local_set_raw = format_ident!("{}set_raw", local_prefix); + let local_drop = format_ident!("{}drop", local_prefix); + let local_deref = format_ident!("{}deref", local_prefix); + let local_deref_mut = format_ident!("{}deref_mut", local_prefix); + + let span = ident.span(); + quote_spanned! {span=> + #[doc(hidden)] + #[export_name = #link_uninit] + unsafe extern "C" fn #local_uninit( + this: *mut ::std::boxed::Box<::std::mem::MaybeUninit<#ident>>, + ) { + ::std::ptr::write( + this, + ::std::boxed::Box::new(::std::mem::MaybeUninit::uninit()), + ); + } + #[doc(hidden)] + #[export_name = #link_set_raw] + unsafe extern "C" fn #local_set_raw( + this: *mut ::std::boxed::Box<#ident>, + raw: *mut #ident, + ) { + ::std::ptr::write(this, ::std::boxed::Box::from_raw(raw)); + } + #[doc(hidden)] + #[export_name = #link_drop] + unsafe extern "C" fn #local_drop(this: *mut ::std::boxed::Box<#ident>) { + ::std::ptr::drop_in_place(this); + } + #[doc(hidden)] + #[export_name = #link_deref] + unsafe extern "C" fn #local_deref( + this: *const ::std::boxed::Box<::std::mem::MaybeUninit<#ident>>, + ) -> *const ::std::mem::MaybeUninit<#ident> { + &**this + } + #[doc(hidden)] + #[export_name = #link_deref_mut] + unsafe extern "C" fn #local_deref_mut( + this: *mut ::std::boxed::Box<::std::mem::MaybeUninit<#ident>>, + ) -> *mut ::std::mem::MaybeUninit<#ident> { + &mut **this + } + } +} + +fn expand_unique_ptr(namespace: &Namespace, ident: &Ident) -> TokenStream { + let prefix = format!("cxxbridge00$unique_ptr${}{}$", namespace, ident); + let link_null = format!("{}null", prefix); + let link_new = format!("{}new", prefix); + let link_raw = format!("{}raw", prefix); + let link_get = format!("{}get", prefix); + let link_release = format!("{}release", prefix); + let link_drop = format!("{}drop", prefix); + + quote! { + unsafe impl ::cxx::private::UniquePtrTarget for #ident { + fn __null() -> *mut ::std::ffi::c_void { + extern "C" { + #[link_name = #link_null] + fn __null(this: *mut *mut ::std::ffi::c_void); + } + let mut repr = ::std::ptr::null_mut::<::std::ffi::c_void>(); + unsafe { __null(&mut repr) } + repr + } + fn __new(mut value: Self) -> *mut ::std::ffi::c_void { + extern "C" { + #[link_name = #link_new] + fn __new(this: *mut *mut ::std::ffi::c_void, value: *mut #ident); + } + let mut repr = ::std::ptr::null_mut::<::std::ffi::c_void>(); + unsafe { __new(&mut repr, &mut value) } + repr + } + unsafe fn __raw(raw: *mut Self) -> *mut ::std::ffi::c_void { + extern "C" { + #[link_name = #link_raw] + fn __raw(this: *mut *mut ::std::ffi::c_void, raw: *mut #ident); + } + let mut repr = ::std::ptr::null_mut::<::std::ffi::c_void>(); + __raw(&mut repr, raw); + repr + } + unsafe fn __get(repr: *mut ::std::ffi::c_void) -> *const Self { + extern "C" { + #[link_name = #link_get] + fn __get(this: *const *mut ::std::ffi::c_void) -> *const #ident; + } + __get(&repr) + } + unsafe fn __release(mut repr: *mut ::std::ffi::c_void) -> *mut Self { + extern "C" { + #[link_name = #link_release] + fn __release(this: *mut *mut ::std::ffi::c_void) -> *mut #ident; + } + __release(&mut repr) + } + unsafe fn __drop(mut repr: *mut ::std::ffi::c_void) { + extern "C" { + #[link_name = #link_drop] + fn __drop(this: *mut *mut ::std::ffi::c_void); + } + __drop(&mut repr); + } + } + } +} + +fn expand_return_type(ret: &Option) -> TokenStream { + match ret { + Some(ret) => quote!(-> #ret), + None => TokenStream::new(), + } +} + +fn indirect_return(ret: &Option, types: &Types) -> bool { + ret.as_ref() + .map_or(false, |ret| types.needs_indirect_abi(ret)) +} + +fn expand_extern_type(ty: &Type) -> TokenStream { + match ty { + Type::Ident(ident) if ident == "String" => quote!(::cxx::private::RustString), + Type::RustBox(ty) | Type::UniquePtr(ty) => { + let inner = &ty.inner; + quote!(*mut #inner) + } + Type::Ref(ty) => match &ty.inner { + Type::Ident(ident) if ident == "String" => quote!(&::cxx::private::RustString), + _ => quote!(#ty), + }, + Type::Str(_) => quote!(::cxx::private::RustStr), + _ => quote!(#ty), + } +} + +fn expand_extern_return_type(ret: &Option, types: &Types) -> TokenStream { + let ret = match ret { + Some(ret) if !types.needs_indirect_abi(ret) => ret, + _ => return TokenStream::new(), + }; + let ty = expand_extern_type(ret); + quote!(-> #ty) +} + +fn expand_extern_arg(arg: &Var, types: &Types) -> TokenStream { + let ident = &arg.ident; + let ty = expand_extern_type(&arg.ty); + if types.needs_indirect_abi(&arg.ty) { + quote!(#ident: *mut #ty) + } else { + quote!(#ident: #ty) + } +} diff --git a/macro/src/lib.rs b/macro/src/lib.rs new file mode 100644 index 00000000..a2117610 --- /dev/null +++ b/macro/src/lib.rs @@ -0,0 +1,46 @@ +#![allow( + clippy::large_enum_variant, + clippy::new_without_default, + clippy::or_fun_call, + clippy::toplevel_ref_arg, + clippy::useless_let_if_seq +)] + +extern crate proc_macro; + +mod expand; +mod namespace; +mod syntax; + +use crate::namespace::Namespace; +use proc_macro::TokenStream; +use syn::{parse_macro_input, ItemMod}; + +/// `#[cxx::bridge] mod ffi { ... }` +/// +/// Refer to the crate-level documentation for the explanation of how this macro +/// is intended to be used. +/// +/// The only additional thing to note here is namespace support — if the +/// types and functions on the `extern "C"` side of our bridge are in a +/// namespace, specify that namespace as an argument of the cxx::bridge +/// attribute macro. +/// +/// ``` +/// #[cxx::bridge(namespace = mycompany::rust)] +/// # mod ffi {} +/// ``` +/// +/// The types and functions from the `extern "Rust"` side of the bridge will be +/// placed into that same namespace in the generated C++ code. +#[proc_macro_attribute] +pub fn bridge(args: TokenStream, input: TokenStream) -> TokenStream { + let _ = syntax::error::ERRORS; + + let namespace = parse_macro_input!(args as Namespace); + let ffi = parse_macro_input!(input as ItemMod); + + expand::bridge(&namespace, ffi) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/macro/src/namespace.rs b/macro/src/namespace.rs new file mode 100644 index 00000000..678e7e00 --- /dev/null +++ b/macro/src/namespace.rs @@ -0,0 +1,39 @@ +use crate::syntax::ident; +use std::fmt::{self, Display}; +use syn::parse::{Parse, ParseStream, Result}; +use syn::{Path, Token}; + +mod kw { + syn::custom_keyword!(namespace); +} + +pub struct Namespace { + segments: Vec, +} + +impl Parse for Namespace { + fn parse(input: ParseStream) -> Result { + let mut segments = Vec::new(); + if !input.is_empty() { + input.parse::()?; + input.parse::()?; + let path = input.call(Path::parse_mod_style)?; + for segment in path.segments { + ident::check(&segment.ident)?; + segments.push(segment.ident.to_string()); + } + input.parse::>()?; + } + Ok(Namespace { segments }) + } +} + +impl Display for Namespace { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for segment in &self.segments { + f.write_str(segment)?; + f.write_str("$")?; + } + Ok(()) + } +} diff --git a/macro/src/syntax b/macro/src/syntax new file mode 120000 index 00000000..8fc0b00e --- /dev/null +++ b/macro/src/syntax @@ -0,0 +1 @@ +../../syntax/ \ No newline at end of file diff --git a/src/cxx_string.rs b/src/cxx_string.rs new file mode 100644 index 00000000..c1697976 --- /dev/null +++ b/src/cxx_string.rs @@ -0,0 +1,91 @@ +use std::borrow::Cow; +use std::fmt::{self, Debug, Display}; +use std::slice; +use std::str::{self, Utf8Error}; + +extern "C" { + #[link_name = "cxxbridge00$cxx_string$data"] + fn string_data(_: &CxxString) -> *const u8; + #[link_name = "cxxbridge00$cxx_string$length"] + fn string_length(_: &CxxString) -> usize; +} + +/// Binding to C++ `std::string`. +/// +/// # Invariants +/// +/// As an invariant of this API and the static analysis of the cxx::bridge +/// macro, in Rust code we can never obtain a `CxxString` by value. C++'s string +/// requires a move constructor and may hold internal pointers, which is not +/// compatible with Rust's move behavior. Instead in Rust code we will only ever +/// look at a CxxString through a reference or smart pointer, as in `&CxxString` +/// or `UniquePtr`. +#[repr(C)] +pub struct CxxString { + _private: [u8; 0], +} + +impl CxxString { + /// Returns the length of the string in bytes. + /// + /// Matches the behavior of C++ [std::string::size][size]. + /// + /// [size]: https://en.cppreference.com/w/cpp/string/basic_string/size + pub fn len(&self) -> usize { + unsafe { string_length(self) } + } + + /// Returns true if `self` has a length of zero bytes. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns a byte slice of this string's contents. + pub fn as_bytes(&self) -> &[u8] { + let data = self.as_ptr(); + let len = self.len(); + unsafe { slice::from_raw_parts(data, len) } + } + + /// Produces a pointer to the first character of the string. + /// + /// Matches the behavior of C++ [std::string::data][data]. + /// + /// Note that the return type may look like `const char *` but is not a + /// `const char *` in the typical C sense, as C++ strings may contain + /// internal null bytes. As such, the returned pointer only makes sense as a + /// string in combination with the length returned by [`len()`](#len). + /// + /// [data]: https://en.cppreference.com/w/cpp/string/basic_string/data + pub fn as_ptr(&self) -> *const u8 { + unsafe { string_data(self) } + } + + /// Validates that the C++ string contains UTF-8 data and produces a view of + /// it as a Rust &str, otherwise an error. + pub fn to_str(&self) -> Result<&str, Utf8Error> { + str::from_utf8(self.as_bytes()) + } + + /// If the contents of the C++ string are valid UTF-8, this function returns + /// a view as a Cow::Borrowed &str. Otherwise replaces any invalid UTF-8 + /// sequences with the U+FFFD [replacement character] and returns a + /// Cow::Owned String. + /// + /// [replacement character]: https://doc.rust-lang.org/std/char/constant.REPLACEMENT_CHARACTER.html + pub fn to_string_lossy(&self) -> Cow { + String::from_utf8_lossy(self.as_bytes()) + } +} + +impl Display for CxxString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&String::from_utf8_lossy(self.as_bytes()), f) + } +} + +impl Debug for CxxString { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Debug::fmt(&String::from_utf8_lossy(self.as_bytes()), f) + } +} diff --git a/src/cxxbridge.cc b/src/cxxbridge.cc new file mode 100644 index 00000000..3d1a9027 --- /dev/null +++ b/src/cxxbridge.cc @@ -0,0 +1,168 @@ +#include "../include/cxxbridge.h" +#include +#include +#include + +namespace cxxbridge = cxxbridge00; + +extern "C" { +const char *cxxbridge00$cxx_string$data(const std::string &s) noexcept { + return s.data(); +} + +size_t cxxbridge00$cxx_string$length(const std::string &s) noexcept { + return s.length(); +} + +// RustString +void cxxbridge00$rust_string$new(cxxbridge::RustString *self) noexcept; +void cxxbridge00$rust_string$clone(cxxbridge::RustString *self, + const cxxbridge::RustString &other) noexcept; +bool cxxbridge00$rust_string$from(cxxbridge::RustString *self, const char *ptr, + size_t len) noexcept; +void cxxbridge00$rust_string$drop(cxxbridge::RustString *self) noexcept; +const char * +cxxbridge00$rust_string$ptr(const cxxbridge::RustString *self) noexcept; +size_t cxxbridge00$rust_string$len(const cxxbridge::RustString *self) noexcept; + +// RustStr +bool cxxbridge00$rust_str$valid(const char *ptr, size_t len) noexcept; +} // extern "C" + +namespace cxxbridge00 { + +RustString::RustString() noexcept { cxxbridge00$rust_string$new(this); } + +RustString::RustString(const RustString &other) noexcept { + cxxbridge00$rust_string$clone(this, other); +} + +RustString::RustString(RustString &&other) noexcept { + this->repr = other.repr; + cxxbridge00$rust_string$new(&other); +} + +RustString::RustString(const char *s) { + auto len = strlen(s); + if (!cxxbridge00$rust_string$from(this, s, len)) { + throw std::invalid_argument("data for RustString is not utf-8"); + } +} + +RustString::RustString(const std::string &s) { + auto ptr = s.data(); + auto len = s.length(); + if (!cxxbridge00$rust_string$from(this, ptr, len)) { + throw std::invalid_argument("data for RustString is not utf-8"); + } +} + +RustString::~RustString() noexcept { cxxbridge00$rust_string$drop(this); } + +RustString::operator std::string() const { + return std::string(this->data(), this->size()); +} + +RustString &RustString::operator=(const RustString &other) noexcept { + if (this != &other) { + cxxbridge00$rust_string$drop(this); + cxxbridge00$rust_string$clone(this, other); + } + return *this; +} + +RustString &RustString::operator=(RustString &&other) noexcept { + if (this != &other) { + cxxbridge00$rust_string$drop(this); + this->repr = other.repr; + cxxbridge00$rust_string$new(&other); + } + return *this; +} + +const char *RustString::data() const noexcept { + return cxxbridge00$rust_string$ptr(this); +} + +size_t RustString::size() const noexcept { + return cxxbridge00$rust_string$len(this); +} + +size_t RustString::length() const noexcept { + return cxxbridge00$rust_string$len(this); +} + +std::ostream &operator<<(std::ostream &os, const RustString &s) { + os.write(s.data(), s.size()); + return os; +} + +RustStr::RustStr() noexcept + : repr(Repr{reinterpret_cast(this), 0}) {} + +RustStr::RustStr(const char *s) : repr(Repr{s, strlen(s)}) { + if (!cxxbridge00$rust_str$valid(this->repr.ptr, this->repr.len)) { + throw std::invalid_argument("data for RustStr is not utf-8"); + } +} + +RustStr::RustStr(const std::string &s) : repr(Repr{s.data(), s.length()}) { + if (!cxxbridge00$rust_str$valid(this->repr.ptr, this->repr.len)) { + throw std::invalid_argument("data for RustStr is not utf-8"); + } +} + +RustStr::RustStr(const RustStr &) noexcept = default; + +RustStr &RustStr::operator=(RustStr other) noexcept { + this->repr = other.repr; + return *this; +} + +RustStr::operator std::string() const { + return std::string(this->data(), this->size()); +} + +const char *RustStr::data() const noexcept { return this->repr.ptr; } + +size_t RustStr::size() const noexcept { return this->repr.len; } + +size_t RustStr::length() const noexcept { return this->repr.len; } + +RustStr::RustStr(Repr repr_) noexcept : repr(repr_) {} + +RustStr::operator Repr() noexcept { return this->repr; } + +std::ostream &operator<<(std::ostream &os, const RustStr &s) { + os.write(s.data(), s.size()); + return os; +} + +} // namespace cxxbridge00 + +extern "C" { +void cxxbridge00$unique_ptr$std$string$null( + std::unique_ptr *ptr) noexcept { + new (ptr) std::unique_ptr(); +} +void cxxbridge00$unique_ptr$std$string$new(std::unique_ptr *ptr, + std::string *value) noexcept { + new (ptr) std::unique_ptr(new std::string(std::move(*value))); +} +void cxxbridge00$unique_ptr$std$string$raw(std::unique_ptr *ptr, + std::string *raw) noexcept { + new (ptr) std::unique_ptr(raw); +} +const std::string *cxxbridge00$unique_ptr$std$string$get( + const std::unique_ptr &ptr) noexcept { + return ptr.get(); +} +std::string *cxxbridge00$unique_ptr$std$string$release( + std::unique_ptr &ptr) noexcept { + return ptr.release(); +} +void cxxbridge00$unique_ptr$std$string$drop( + std::unique_ptr *ptr) noexcept { + ptr->~unique_ptr(); +} +} // extern "C" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..a05f7c8a --- /dev/null +++ b/src/error.rs @@ -0,0 +1,16 @@ +use std::io; +use std::path::StripPrefixError; +use thiserror::Error; + +pub(super) type Result = std::result::Result; + +#[derive(Error, Debug)] +#[error(transparent)] +pub(super) enum Error { + #[error("missing OUT_DIR environment variable")] + MissingOutDir, + #[error("failed to locate target dir")] + TargetDir, + Io(#[from] io::Error), + StripPrefix(#[from] StripPrefixError), +} diff --git a/src/gen b/src/gen new file mode 120000 index 00000000..334e0fbd --- /dev/null +++ b/src/gen @@ -0,0 +1 @@ +../gen \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..8ed95722 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,458 @@ +//! This library provides a **safe** mechanism for calling C++ code from Rust +//! and Rust code from C++, not subject to the many ways that things can go +//! wrong when using bindgen or cbindgen to generate unsafe C-style bindings. +//! +//!
+//! +//! *Compiler support: requires rustc 1.42+ (beta on January 30, stable on March +//! 12)* +//! +//!
+//! +//! # Overview +//! +//! The idea is that we define the signatures of both sides of our FFI boundary +//! embedded together in one Rust module (the next section shows an example). +//! From this, CXX receives a complete picture of the boundary to perform static +//! analyses against the types and function signatures to uphold both Rust's and +//! C++'s invariants and requirements. +//! +//! If everything checks out statically, then CXX uses a pair of code generators +//! to emit the relevant `extern "C"` signatures on both sides together with any +//! necessary static assertions for later in the build process to verify +//! correctness. On the Rust side this code generator is simply an attribute +//! procedural macro. On the C++ side it can be a small Cargo build script if +//! your build is managed by Cargo, or for other build systems like Bazel or +//! Buck we provide a command line tool which generates the header and source +//! file and should be easy to integrate. +//! +//! The resulting FFI bridge operates at zero or negligible overhead, i.e. no +//! copying, no serialization, no memory allocation, no runtime checks needed. +//! +//! The FFI signatures are able to use native types from whichever side they +//! please, such as Rust's `String` or C++'s `std::string`, Rust's `Box` or +//! C++'s `std::unique_ptr`, Rust's `Vec` or C++'s `std::vector`, etc in any +//! combination. CXX guarantees an ABI-compatible signature that both sides +//! understand, based on builtin bindings for key standard library types to +//! expose an idiomatic API on those types to the other language. For example +//! when manipulating a C++ string from Rust, its `len()` method becomes a call +//! of the `size()` member function defined by C++; when manipulation a Rust +//! string from C++, its `size()` member function calls Rust's `len()`. +//! +//!
+//! +//! # Example +//! +//! A runnable version of this example is provided under the *demo-rs* directory +//! of https://github.com/dtolnay/cxx (with the C++ side of the implementation +//! in the *demo-cxx* directory). To try it out, jump into demo-rs and run +//! `cargo run`. +//! +//! ```no_run +//! #[cxx::bridge] +//! mod ffi { +//! // Any shared structs, whose fields will be visible to both languages. +//! struct SharedThing { +//! z: i32, +//! y: Box, +//! x: UniquePtr, +//! } +//! +//! extern "C" { +//! // One or more headers with the matching C++ declarations. Our code +//! // generators don't read it but it gets #include'd and used in static +//! // assertions to ensure our picture of the FFI boundary is accurate. +//! include!("demo-cxx/demo.h"); +//! +//! // Zero or more opaque types which both languages can pass around but +//! // only C++ can see the fields. +//! type ThingC; +//! +//! // Functions implemented in C++. +//! fn make_demo(appname: &str) -> UniquePtr; +//! fn get_name(thing: &ThingC) -> &CxxString; +//! fn do_thing(state: SharedThing); +//! } +//! +//! extern "Rust" { +//! // Zero or more opaque types which both languages can pass around but +//! // only Rust can see the fields. +//! type ThingR; +//! +//! // Functions implemented in Rust. +//! fn print_r(r: &ThingR); +//! } +//! } +//! # +//! # pub struct ThingR(usize); +//! # +//! # fn print_r(r: &ThingR) { +//! # println!("called back with r={}", r.0); +//! # } +//! # +//! # fn main() {} +//! ``` +//! +//! Now we simply provide C++ definitions of all the things in the `extern "C"` +//! block and Rust definitions of all the things in the `extern "Rust"` block, +//! and get to call back and forth safely. +//! +//! Here are links to the complete set of source files involved in the demo: +//! +//! - [demo-rs/src/main.rs](https://github.com/dtolnay/cxx/blob/master/demo-rs/src/main.rs) +//! - [demo-rs/build.rs](https://github.com/dtolnay/cxx/blob/master/demo-rs/build.rs) +//! - [demo-cxx/demo.h](https://github.com/dtolnay/cxx/blob/master/demo-cxx/demo.h) +//! - [demo-cxx/demo.cc](https://github.com/dtolnay/cxx/blob/master/demo-cxx/demo.cc) +//! +//! To look at the code generated in both languages for the example by the CXX +//! code generators: +//! +//! ```console +//! # run Rust code generator and print to stdout +//! # (requires https://github.com/dtolnay/cargo-expand) +//! $ cargo expand --manifest-path demo-rs/Cargo.toml +//! +//! # run C++ code generator and print to stdout +//! $ cargo run --manifest-path cmd/Cargo.toml -- demo-rs/src/main.rs +//! ``` +//! +//!
+//! +//! # Details +//! +//! As seen in the example, the language of the FFI boundary involves 3 kinds of +//! items: +//! +//! - **Shared structs** — their fields are made visible to both +//! languages. The definition written within cxx::bridge is the single source +//! of truth. +//! +//! - **Opaque types** — their fields are secret from the other language. +//! These cannot be passed across the FFI by value but only behind an +//! indirection, such as a reference `&`, a Rust `Box`, or a `UniquePtr`. Can +//! be a type alias for an arbitrarily complicated generic language-specific +//! type depending on your use case. +//! +//! - **Functions** — implemented in either language, callable from the +//! other language. +//! +//! Within the `extern "C"` part of the CXX bridge we list the types and +//! functions for which C++ is the source of truth, as well as the header(s) +//! that declare those APIs. In the future it's possible that this section could +//! be generated bindgen-style from the headers but for now we need the +//! signatures written out; static assertions will verify that they are +//! accurate. +//! +//! Within the `extern "Rust"` part, we list types and functions for which Rust +//! is the source of truth. These all implicitly refer to the `super` module, +//! the parent module of the CXX bridge. You can think of the two items listed +//! in the example above as being like `use super::ThingR` and `use +//! super::print_r` except re-exported to C++. The parent module will either +//! contain the definitions directly for simple things, or contain the relevant +//! `use` statements to bring them into scope from elsewhere. +//! +//! Your function implementations themselves, whether in C++ or Rust, *do not* +//! need to be defined as `extern "C"` ABI or no\_mangle. CXX will put in the +//! right shims where necessary to make it all work. +//! +//!
+//! +//! # Comparison vs bindgen and cbindgen +//! +//! Notice that with CXX there is repetition of all the function signatures: +//! they are typed out once where the implementation is defined (in C++ or Rust) +//! and again inside the cxx::bridge module, though compile-time assertions +//! guarantee these are kept in sync. This is different from [bindgen] and +//! [cbindgen] where function signatures are typed by a human once and the tool +//! consumes them in one language and emits them in the other language. +//! +//! [bindgen]: https://github.com/rust-lang/rust-bindgen +//! [cbindgen]: https://github.com/eqrion/cbindgen/ +//! +//! This is because CXX fills a somewhat different role. It is a lower level +//! tool than bindgen or cbindgen in a sense; you can think of it as being a +//! replacement for the concept of `extern "C"` signatures as we know them, +//! rather than a replacement for a bindgen. It would be reasonable to build a +//! higher level bindgen-like tool on top of CXX which consumes a C++ header +//! and/or Rust module (and/or IDL like Thrift) as source of truth and generates +//! the cxx::bridge, eliminating the repetition while leveraging the static +//! analysis safety guarantees of CXX. +//! +//! But note in other ways CXX is higher level than the bindgens, with rich +//! support for common standard library types. Frequently with bindgen when we +//! are dealing with an idiomatic C++ API we would end up manually wrapping that +//! API in C-style raw pointer functions, applying bindgen to get unsafe raw +//! pointer Rust functions, and replicating the API again to expose those +//! idiomatically in Rust. That's a much worse form of repetition because it is +//! unsafe all the way through. +//! +//! By using a CXX bridge as the shared understanding between the languages, +//! rather than `extern "C"` C-style signatures as the shared understanding, +//! common FFI use cases become expressible using 100% safe code. +//! +//! It would also be reasonable to mix and match, using CXX bridge for the 95% +//! of your FFI that is straightforward and doing the remaining few oddball +//! signatures the old fashioned way with bindgen and cbindgen, if for some +//! reason CXX's static restrictions get in the way. Please file an issue if you +//! end up taking this approach so that we know what ways it would be worthwhile +//! to make the tool more expressive. +//! +//!
+//! +//! # Cargo-based setup +//! +//! For builds that are orchestrated by Cargo, you will use a build script that +//! runs CXX's C++ code generator and compiles the resulting C++ code along with +//! any other C++ code for your crate. +//! +//! The canonical build script is as follows. The indicated line returns a +//! [`cc::Build`] instance (from the usual widely used `cc` crate) on which you +//! can set up any additional source files and compiler flags as normal. +//! +//! [`cc::Build`]: https://docs.rs/cc/1.0/cc/struct.Build.html +//! +//! ```no_run +//! // build.rs +//! +//! fn main() { +//! cxx::Build::new() +//! .bridge("src/main.rs") // returns a cc::Build +//! .file("../demo-cxx/demo.cc") +//! .flag("-std=c++11") +//! .compile("cxxbridge-demo"); +//! +//! println!("cargo:rerun-if-changed=src/main.rs"); +//! println!("cargo:rerun-if-changed=../demo-cxx/demo.h"); +//! println!("cargo:rerun-if-changed=../demo-cxx/demo.cc"); +//! } +//! ``` +//! +//!

+//! +//! # Non-Cargo setup +//! +//! For use in non-Cargo builds like Bazel or Buck, CXX provides an alternate +//! way of invoking the C++ code generator as a standalone command line tool. +//! The tool is packaged as the `cxxbridge-cmd` crate on crates.io or can be +//! built from the *cmd* directory of https://github.com/dtolnay/cxx. +//! +//! ```bash +//! $ cargo install cxxbridge-cmd +//! +//! $ cxxbridge src/main.rs --header > path/to/mybridge.h +//! $ cxxbridge src/main.rs > path/to/mybridge.cc +//! ``` +//! +//!
+//! +//! # Safety +//! +//! Be aware that the design of this library is intentionally restrictive and +//! opinionated! It isn't a goal to be powerful enough to handle arbitrary +//! signatures in either language. Instead this project is about carving out a +//! reasonably expressive set of functionality about which we can make useful +//! safety guarantees today and maybe extend over time. You may find that it +//! takes some practice to use CXX bridge effectively as it won't work in all +//! the ways that you are used to. +//! +//! Some of the considerations that go into ensuring safety are: +//! +//! - By design, our paired code generators work together to control both sides +//! of the FFI boundary. Ordinarily in Rust writing your own `extern "C"` +//! blocks is unsafe because the Rust compiler has no way to know whether the +//! signatures you've written actually match the signatures implemented in the +//! other language. With CXX we achieve that visibility and know what's on the +//! other side. +//! +//! - Our static analysis detects and prevents passing types by value that +//! shouldn't be passed by value from C++ to Rust, for example because they +//! may contain internal pointers that would be screwed up by Rust's move +//! behavior. +//! +//! - To many people's surprise, it is possible to have a struct in Rust and a +//! struct in C++ with exactly the same layout / fields / alignment / +//! everything, and still not the same ABI when passed by value. This is a +//! longstanding bindgen bug that leads to segfaults in absolutely +//! correct-looking code ([rust-lang/rust-bindgen#778]). CXX knows about this +//! and can insert the necessary zero-cost workaround transparently where +//! needed, so go ahead and pass your structs by value without worries. This +//! is made possible by owning both sides of the boundary rather than just +//! one. +//! +//! - Template instantiations: for example in order to expose a UniquePtr\ +//! type in Rust backed by a real C++ unique\_ptr, we have a way of using a +//! Rust trait to connect the behavior back to the template instantiations +//! performed by the other language. +//! +//! [rust-lang/rust-bindgen#778]: https://github.com/rust-lang/rust-bindgen/issues/778 +//! +//!
+//! +//! # Builtin types +//! +//! In addition to all the primitive types (i32 ⟷ int32_t), the following common +//! types may be used in the fields of shared structs and the arguments and +//! returns of functions. +//! +//! +//! +//! +//! +//! +//! +//! +//! +//!
name in Rustname in C++restrictions
Stringcxxbridge::RustString
&strcxxbridge::RustStr
CxxStringstd::stringcannot be passed by value
Box<T>cxxbridge::RustBox<T>cannot hold opaque C++ type
UniquePtr<T>std::unique_ptr<T>cannot hold opaque Rust type
+//! +//! The C++ API of the `cxxbridge` namespace is defined by the +//! *include/cxxbridge.h* file in https://github.com/dtolnay/cxx. You will need +//! to include this header in your C++ code when working with those types. +//! +//! The following types are intended to be supported "soon" but are just not +//! implemented yet. I don't expect any of these to be hard to make work but +//! it's a matter of designing a nice API for each in its non-native language. +//! +//! +//! +//! +//! +//! +//! +//! +//! +//! +//!
name in Rustname in C++
&[T]
Vec<T>
BTreeMap<K, V>
HashMap<K, V>
std::vector<T>
std::map<K, V>
std::unordered_map<K, V>
+ +#![deny(improper_ctypes)] +#![allow( + clippy::large_enum_variant, + clippy::missing_safety_doc, + clippy::module_inception, + clippy::new_without_default, + clippy::or_fun_call, + clippy::ptr_arg, + clippy::toplevel_ref_arg, + clippy::transmute_ptr_to_ptr, + clippy::useless_let_if_seq +)] + +mod cxx_string; +mod error; +mod gen; +mod opaque; +mod paths; +mod rust_str; +mod rust_string; +mod syntax; +mod unique_ptr; +mod unwind; + +pub use crate::cxx_string::CxxString; +pub use crate::unique_ptr::UniquePtr; +pub use cxxbridge_macro::bridge; + +// Not public API. +#[doc(hidden)] +pub mod private { + pub use crate::opaque::Opaque; + pub use crate::rust_str::RustStr; + pub use crate::rust_string::RustString; + pub use crate::unique_ptr::UniquePtrTarget; + pub use crate::unwind::catch_unwind; +} + +use crate::error::Result; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; +use std::process; + +/// The CXX code generator for constructing and compiling C++ code. +/// +/// This is intended to be used from Cargo build scripts to execute CXX's +/// C++ code generator, set up any additional compiler flags depending on +/// the use case, and make the C++ compiler invocation. +/// +///
+/// +/// # Example +/// +/// Example of a canonical Cargo build script that builds a CXX bridge: +/// +/// ```no_run +/// // build.rs +/// +/// fn main() { +/// cxx::Build::new() +/// .bridge("src/main.rs") +/// .file("../demo-cxx/demo.cc") +/// .flag("-std=c++11") +/// .compile("cxxbridge-demo"); +/// +/// println!("cargo:rerun-if-changed=src/main.rs"); +/// println!("cargo:rerun-if-changed=../demo-cxx/demo.h"); +/// println!("cargo:rerun-if-changed=../demo-cxx/demo.cc"); +/// } +/// ``` +/// +/// A runnable working setup with this build script is shown in the +/// *demo-rs* and *demo-cxx* directories of +/// [https://github.com/dtolnay/cxx](https://github.com/dtolnay/cxx). +/// +///
+/// +/// # Alternatives +/// +/// For use in non-Cargo builds like Bazel or Buck, CXX provides an +/// alternate way of invoking the C++ code generator as a standalone command +/// line tool. The tool is packaged as the `cxxbridge-cmd` crate. +/// +/// ```bash +/// $ cargo install cxxbridge-cmd # or build it from the repo +/// +/// $ cxxbridge src/main.rs --header > path/to/mybridge.h +/// $ cxxbridge src/main.rs > path/to/mybridge.cc +/// ``` +#[must_use] +pub struct Build { + _private: (), +} + +impl Build { + /// Begin with a [`cc::Build`] in its default configuration. + pub fn new() -> Self { + Build { _private: () } + } + + /// This returns a [`cc::Build`] on which you should continue to set up + /// any additional source files or compiler flags, and lastly call its + /// [`compile`] method to execute the C++ build. + /// + /// [`compile`]: https://docs.rs/cc/1.0.49/cc/struct.Build.html#method.compile + #[must_use] + pub fn bridge(&self, rust_source_file: impl AsRef) -> cc::Build { + match try_generate_bridge(rust_source_file.as_ref()) { + Ok(build) => build, + Err(err) => { + let _ = writeln!(io::stderr(), "\n\ncxxbridge error: {}\n\n", err); + process::exit(1); + } + } + } +} + +fn try_generate_bridge(rust_source_file: &Path) -> Result { + let header = gen::do_generate_header(rust_source_file); + let header_path = paths::out_with_extension(rust_source_file, ".h")?; + fs::create_dir_all(header_path.parent().unwrap())?; + fs::write(&header_path, header)?; + paths::symlink_header(&header_path, rust_source_file); + + let bridge = gen::do_generate_bridge(rust_source_file); + let bridge_path = paths::out_with_extension(rust_source_file, ".cc")?; + fs::write(&bridge_path, bridge)?; + let mut build = paths::cc_build(); + build.file(&bridge_path); + + Ok(build) +} diff --git a/src/opaque.rs b/src/opaque.rs new file mode 100644 index 00000000..ded64e91 --- /dev/null +++ b/src/opaque.rs @@ -0,0 +1,16 @@ +use std::mem; + +// . size = 0 +// . align = 1 +// . ffi-safe +// . !Send +// . !Sync +#[repr(C, packed)] +pub struct Opaque { + _private: [*const u8; 0], +} + +fn _assert() { + let _: [(); 0] = [(); mem::size_of::()]; + let _: [(); 1] = [(); mem::align_of::()]; +} diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 00000000..866d3774 --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,72 @@ +use crate::error::{Error, Result}; +use std::env; +use std::fs; +use std::os; +use std::path::{Path, PathBuf}; + +fn out_dir() -> Result { + env::var_os("OUT_DIR") + .map(PathBuf::from) + .ok_or(Error::MissingOutDir) +} + +pub(crate) fn cc_build() -> cc::Build { + try_cc_build().unwrap_or_default() +} + +fn try_cc_build() -> Result { + let target_dir = target_dir()?; + + let mut build = cc::Build::new(); + build.include(target_dir.join("cxxbridge")); + build.include(target_dir.parent().unwrap()); + Ok(build) +} + +// Symlink the header file into a predictable place. The header generated from +// path/to/mod.rs gets linked to targets/cxxbridge/path/to/mod.h. +pub(crate) fn symlink_header(path: &Path, original: &Path) { + let _ = try_symlink_header(path, original); +} + +fn try_symlink_header(path: &Path, original: &Path) -> Result<()> { + let suffix = relative_to_parent_of_target_dir(original)?; + let dst = target_dir()?.join("cxxbridge").join(suffix); + + fs::create_dir_all(dst.parent().unwrap())?; + #[cfg(unix)] + os::unix::fs::symlink(path, dst)?; + #[cfg(windows)] + os::windows::fs::symlink_file(path, dst)?; + + Ok(()) +} + +fn relative_to_parent_of_target_dir(original: &Path) -> Result { + let target_dir = target_dir()?; + let parent_of_target_dir = target_dir.parent().unwrap(); + let original = original.canonicalize()?; + let suffix = original.strip_prefix(parent_of_target_dir)?; + Ok(suffix.to_owned()) +} + +pub(crate) fn out_with_extension(path: &Path, ext: &str) -> Result { + let mut file_name = path.file_name().unwrap().to_owned(); + file_name.push(ext); + + let out_dir = out_dir()?; + let rel = relative_to_parent_of_target_dir(path)?; + Ok(out_dir.join(rel).with_file_name(file_name)) +} + +fn target_dir() -> Result { + let mut dir = out_dir()?.canonicalize()?; + loop { + if dir.ends_with("target") { + return Ok(dir); + } + if !dir.pop() { + return Err(Error::TargetDir); + } + } +} diff --git a/src/rust_str.rs b/src/rust_str.rs new file mode 100644 index 00000000..5aeac282 --- /dev/null +++ b/src/rust_str.rs @@ -0,0 +1,30 @@ +use std::slice; +use std::str; + +// Not necessarily ABI compatible with &str. Codegen performs the translation. +#[repr(C)] +#[derive(Copy, Clone)] +pub struct RustStr { + ptr: *const u8, + len: usize, +} + +impl RustStr { + pub fn from(s: &str) -> Self { + RustStr { + ptr: s.as_ptr(), + len: s.len(), + } + } + + pub unsafe fn as_str<'a>(self) -> &'a str { + let slice = slice::from_raw_parts(self.ptr, self.len); + str::from_utf8_unchecked(slice) + } +} + +#[export_name = "cxxbridge00$rust_str$valid"] +unsafe extern "C" fn str_valid(ptr: *const u8, len: usize) -> bool { + let slice = slice::from_raw_parts(ptr, len); + str::from_utf8(slice).is_ok() +} diff --git a/src/rust_string.rs b/src/rust_string.rs new file mode 100644 index 00000000..5756efad --- /dev/null +++ b/src/rust_string.rs @@ -0,0 +1,73 @@ +use std::mem::{self, ManuallyDrop, MaybeUninit}; +use std::ptr; +use std::slice; +use std::str; + +#[repr(C)] +pub struct RustString { + repr: String, +} + +impl RustString { + pub fn from(s: String) -> Self { + RustString { repr: s } + } + + pub fn from_ref(s: &String) -> &Self { + unsafe { std::mem::transmute::<&String, &RustString>(s) } + } + + pub fn into_string(self) -> String { + self.repr + } + + pub fn as_string(&self) -> &String { + &self.repr + } +} + +#[export_name = "cxxbridge00$rust_string$new"] +unsafe extern "C" fn string_new(this: &mut MaybeUninit) { + ptr::write(this.as_mut_ptr(), String::new()); +} + +#[export_name = "cxxbridge00$rust_string$clone"] +unsafe extern "C" fn string_clone(this: &mut MaybeUninit, other: &String) { + ptr::write(this.as_mut_ptr(), other.clone()); +} + +#[export_name = "cxxbridge00$rust_string$from"] +unsafe extern "C" fn string_from( + this: &mut MaybeUninit, + ptr: *const u8, + len: usize, +) -> bool { + let slice = slice::from_raw_parts(ptr, len); + match str::from_utf8(slice) { + Ok(s) => { + ptr::write(this.as_mut_ptr(), s.to_owned()); + true + } + Err(_) => false, + } +} + +#[export_name = "cxxbridge00$rust_string$drop"] +unsafe extern "C" fn string_drop(this: &mut ManuallyDrop) { + ManuallyDrop::drop(this); +} + +#[export_name = "cxxbridge00$rust_string$ptr"] +unsafe extern "C" fn string_ptr(this: &String) -> *const u8 { + this.as_ptr() +} + +#[export_name = "cxxbridge00$rust_string$len"] +unsafe extern "C" fn string_len(this: &String) -> usize { + this.len() +} + +fn _assert() { + let _: [(); mem::size_of::<[usize; 3]>()] = [(); mem::size_of::()]; + let _: [(); mem::align_of::()] = [(); mem::align_of::()]; +} diff --git a/src/syntax b/src/syntax new file mode 120000 index 00000000..f4007125 --- /dev/null +++ b/src/syntax @@ -0,0 +1 @@ +../syntax \ No newline at end of file diff --git a/src/unique_ptr.rs b/src/unique_ptr.rs new file mode 100644 index 00000000..f6d2e863 --- /dev/null +++ b/src/unique_ptr.rs @@ -0,0 +1,173 @@ +use crate::cxx_string::CxxString; +use std::ffi::c_void; +use std::fmt::{self, Debug, Display}; +use std::marker::PhantomData; +use std::mem::{self, MaybeUninit}; +use std::ptr; + +/// Binding to C++ `std::unique_ptr>`. +#[repr(C)] +pub struct UniquePtr +where + T: UniquePtrTarget, +{ + repr: *mut c_void, + ty: PhantomData, +} + +impl UniquePtr +where + T: UniquePtrTarget, +{ + /// Makes a new UniquePtr wrapping a null pointer. + /// + /// Matches the behavior of default-constructing a std::unique\_ptr. + pub fn null() -> Self { + UniquePtr { + repr: T::__null(), + ty: PhantomData, + } + } + + /// Allocates memory on the heap and makes a UniquePtr pointing to it. + pub fn new(value: T) -> Self { + UniquePtr { + repr: T::__new(value), + ty: PhantomData, + } + } + + /// Checks whether the UniquePtr does not own an object. + /// + /// This is the opposite of [std::unique_ptr\::operator bool](https://en.cppreference.com/w/cpp/memory/unique_ptr/operator_bool). + pub fn is_null(&self) -> bool { + let ptr = unsafe { T::__get(self.repr) }; + ptr.is_null() + } + + /// Returns a reference to the object owned by this UniquePtr if any, + /// otherwise None. + pub fn as_ref(&self) -> Option<&T> { + unsafe { T::__get(self.repr).as_ref() } + } + + /// Consumes the UniquePtr, releasing its ownership of the heap-allocated T. + /// + /// Matches the behavior of [std::unique_ptr\::release](https://en.cppreference.com/w/cpp/memory/unique_ptr/release). + pub fn into_raw(self) -> *mut T { + let ptr = unsafe { T::__release(self.repr) }; + mem::forget(self); + ptr + } + + /// Constructs a UniquePtr retaking ownership of a pointer previously + /// obtained from `into_raw`. + /// + /// # Safety + /// + /// This function is unsafe because improper use may lead to memory + /// problems. For example a double-free may occur if the function is called + /// twice on the same raw pointer. + pub unsafe fn from_raw(raw: *mut T) -> Self { + UniquePtr { + repr: T::__raw(raw), + ty: PhantomData, + } + } +} + +unsafe impl Send for UniquePtr where T: Send + UniquePtrTarget {} +unsafe impl Sync for UniquePtr where T: Sync + UniquePtrTarget {} + +impl Drop for UniquePtr +where + T: UniquePtrTarget, +{ + fn drop(&mut self) { + unsafe { T::__drop(self.repr) } + } +} + +impl Debug for UniquePtr +where + T: Debug + UniquePtrTarget, +{ + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self.as_ref() { + None => formatter.write_str("nullptr"), + Some(value) => Debug::fmt(value, formatter), + } + } +} + +impl Display for UniquePtr +where + T: Display + UniquePtrTarget, +{ + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self.as_ref() { + None => formatter.write_str("nullptr"), + Some(value) => Display::fmt(value, formatter), + } + } +} + +// Methods are private; not intended to be implemented outside of cxxbridge +// codebase. +pub unsafe trait UniquePtrTarget { + #[doc(hidden)] + fn __null() -> *mut c_void; + #[doc(hidden)] + fn __new(value: Self) -> *mut c_void; + #[doc(hidden)] + unsafe fn __raw(raw: *mut Self) -> *mut c_void; + #[doc(hidden)] + unsafe fn __get(repr: *mut c_void) -> *const Self; + #[doc(hidden)] + unsafe fn __release(repr: *mut c_void) -> *mut Self; + #[doc(hidden)] + unsafe fn __drop(repr: *mut c_void); +} + +extern "C" { + #[link_name = "cxxbridge00$unique_ptr$std$string$null"] + fn unique_ptr_std_string_null(this: *mut *mut c_void); + #[link_name = "cxxbridge00$unique_ptr$std$string$new"] + fn unique_ptr_std_string_new(this: *mut *mut c_void, value: *mut CxxString); + #[link_name = "cxxbridge00$unique_ptr$std$string$raw"] + fn unique_ptr_std_string_raw(this: *mut *mut c_void, raw: *mut CxxString); + #[link_name = "cxxbridge00$unique_ptr$std$string$get"] + fn unique_ptr_std_string_get(this: *const *mut c_void) -> *const CxxString; + #[link_name = "cxxbridge00$unique_ptr$std$string$release"] + fn unique_ptr_std_string_release(this: *mut *mut c_void) -> *mut CxxString; + #[link_name = "cxxbridge00$unique_ptr$std$string$drop"] + fn unique_ptr_std_string_drop(this: *mut *mut c_void); +} + +unsafe impl UniquePtrTarget for CxxString { + fn __null() -> *mut c_void { + let mut repr = ptr::null_mut::(); + unsafe { unique_ptr_std_string_null(&mut repr) } + repr + } + fn __new(value: Self) -> *mut c_void { + let mut repr = ptr::null_mut::(); + let mut value = MaybeUninit::new(value); + unsafe { unique_ptr_std_string_new(&mut repr, value.as_mut_ptr() as *mut Self) } + repr + } + unsafe fn __raw(raw: *mut Self) -> *mut c_void { + let mut repr = ptr::null_mut::(); + unique_ptr_std_string_raw(&mut repr, raw); + repr + } + unsafe fn __get(repr: *mut c_void) -> *const Self { + unique_ptr_std_string_get(&repr) + } + unsafe fn __release(mut repr: *mut c_void) -> *mut Self { + unique_ptr_std_string_release(&mut repr) + } + unsafe fn __drop(mut repr: *mut c_void) { + unique_ptr_std_string_drop(&mut repr); + } +} diff --git a/src/unwind.rs b/src/unwind.rs new file mode 100644 index 00000000..070aa28f --- /dev/null +++ b/src/unwind.rs @@ -0,0 +1,20 @@ +use std::io::{self, Write}; +use std::panic::{self, UnwindSafe}; +use std::process; + +pub fn catch_unwind(label: &'static str, foreign_call: F) -> R +where + F: FnOnce() -> R + UnwindSafe, +{ + match panic::catch_unwind(foreign_call) { + Ok(ret) => ret, + Err(_) => abort(label), + } +} + +#[cold] +fn abort(label: &'static str) -> ! { + let mut stdout = io::stdout(); + let _ = writeln!(stdout, "Error: panic in ffi function {}, aborting.", label); + process::abort(); +} diff --git a/syntax/atom.rs b/syntax/atom.rs new file mode 100644 index 00000000..57325d82 --- /dev/null +++ b/syntax/atom.rs @@ -0,0 +1,40 @@ +use proc_macro2::Ident; + +#[derive(Copy, Clone, PartialEq)] +pub enum Atom { + Bool, + U8, + U16, + U32, + U64, + Usize, + I8, + I16, + I32, + I64, + Isize, + CxxString, + RustString, +} + +impl Atom { + pub fn from(ident: &Ident) -> Option { + use self::Atom::*; + match ident.to_string().as_str() { + "bool" => Some(Bool), + "u8" => Some(U8), + "u16" => Some(U16), + "u32" => Some(U32), + "u64" => Some(U64), + "usize" => Some(Usize), + "i8" => Some(I8), + "i16" => Some(I16), + "i32" => Some(I32), + "i64" => Some(I64), + "isize" => Some(Isize), + "CxxString" => Some(CxxString), + "String" => Some(RustString), + _ => None, + } + } +} diff --git a/syntax/attrs.rs b/syntax/attrs.rs new file mode 100644 index 00000000..6d661ba7 --- /dev/null +++ b/syntax/attrs.rs @@ -0,0 +1,68 @@ +use crate::syntax::{Derive, Doc}; +use proc_macro2::Ident; +use syn::parse::{ParseStream, Parser}; +use syn::{Attribute, Error, LitStr, Path, Result, Token}; + +pub(super) fn parse_doc(attrs: &[Attribute]) -> Result { + let mut doc = Doc::new(); + let derives = None; + parse(attrs, &mut doc, derives)?; + Ok(doc) +} + +pub(super) fn parse( + attrs: &[Attribute], + doc: &mut Doc, + mut derives: Option<&mut Vec>, +) -> Result<()> { + for attr in attrs { + if attr.path.is_ident("doc") { + let lit = parse_doc_attribute.parse2(attr.tokens.clone())?; + doc.push(lit); + continue; + } else if attr.path.is_ident("derive") { + if let Some(derives) = &mut derives { + derives.extend(attr.parse_args_with(parse_derive_attribute)?); + continue; + } + } + return Err(Error::new_spanned(attr, "unsupported attribute")); + } + Ok(()) +} + +fn parse_doc_attribute(input: ParseStream) -> Result { + input.parse::()?; + let lit: LitStr = input.parse()?; + Ok(lit) +} + +fn parse_derive_attribute(input: ParseStream) -> Result> { + input + .parse_terminated::(Path::parse_mod_style)? + .into_iter() + .map(|path| match path.get_ident() { + Some(ident) if Derive::from(ident).is_some() => Ok(ident.clone()), + _ => Err(Error::new_spanned(path, "unsupported derive")), + }) + .collect() +} + +impl Derive { + pub fn from(ident: &Ident) -> Option { + match ident.to_string().as_str() { + "Clone" => Some(Derive::Clone), + "Copy" => Some(Derive::Copy), + _ => None, + } + } +} + +impl AsRef for Derive { + fn as_ref(&self) -> &str { + match self { + Derive::Clone => "Clone", + Derive::Copy => "Copy", + } + } +} diff --git a/syntax/check.rs b/syntax/check.rs new file mode 100644 index 00000000..c2c6d41b --- /dev/null +++ b/syntax/check.rs @@ -0,0 +1,203 @@ +use crate::syntax::atom::Atom::{self, *}; +use crate::syntax::{error, ident, Api, ExternFn, Ty1, Type, Types, Var}; +use proc_macro2::Ident; +use syn::{Error, Result}; + +pub(crate) fn typecheck(apis: &[Api], types: &Types) -> Result<()> { + let mut errors = Vec::new(); + + for ty in types { + match ty { + Type::Ident(ident) => { + if Atom::from(ident).is_none() + && !types.structs.contains_key(ident) + && !types.cxx.contains(ident) + && !types.rust.contains(ident) + { + errors.push(unsupported_type(ident)); + } + } + Type::RustBox(ptr) => { + if let Type::Ident(ident) = &ptr.inner { + if types.cxx.contains(ident) { + errors.push(unsupported_cxx_type_in_box(ptr)); + } + if Atom::from(ident).is_none() { + continue; + } + } + errors.push(unsupported_box_target(ptr)); + } + Type::UniquePtr(ptr) => { + if let Type::Ident(ident) = &ptr.inner { + if types.rust.contains(ident) { + errors.push(unsupported_rust_type_in_unique_ptr(ptr)); + } + match Atom::from(ident) { + None | Some(CxxString) => continue, + _ => {} + } + } + errors.push(unsupported_unique_ptr_target(ptr)); + } + _ => {} + } + } + + for api in apis { + match api { + Api::Struct(strct) => { + for field in &strct.fields { + if is_unsized(&field.ty, types) { + errors.push(field_by_value(field, types)); + } + } + } + Api::CxxFunction(efn) | Api::RustFunction(efn) => { + for arg in &efn.args { + if is_unsized(&arg.ty, types) { + errors.push(argument_by_value(arg, types)); + } + } + if let Some(ty) = &efn.ret { + if is_unsized(ty, types) { + errors.push(return_by_value(ty, types)); + } + } + } + _ => {} + } + } + + for api in apis { + if let Api::CxxFunction(efn) = api { + errors.extend(check_mut_return_restriction(efn).err()); + } + if let Api::CxxFunction(efn) | Api::RustFunction(efn) = api { + errors.extend(check_multiple_arg_lifetimes(efn).err()); + } + } + + ident::check_all(apis, &mut errors); + + let mut iter = errors.into_iter(); + let mut all_errors = match iter.next() { + Some(err) => err, + None => return Ok(()), + }; + for err in iter { + all_errors.combine(err); + } + Err(all_errors) +} + +fn is_unsized(ty: &Type, types: &Types) -> bool { + let ident = match ty { + Type::Ident(ident) => ident, + _ => return false, + }; + ident == "CxxString" || types.cxx.contains(ident) || types.rust.contains(ident) +} + +fn check_mut_return_restriction(efn: &ExternFn) -> Result<()> { + match &efn.ret { + Some(Type::Ref(ty)) if ty.mutability.is_some() => {} + _ => return Ok(()), + } + + for arg in &efn.args { + if let Type::Ref(ty) = &arg.ty { + if ty.mutability.is_some() { + return Ok(()); + } + } + } + + Err(Error::new_spanned( + efn, + "&mut return type is not allowed unless there is a &mut argument", + )) +} + +fn check_multiple_arg_lifetimes(efn: &ExternFn) -> Result<()> { + match &efn.ret { + Some(Type::Ref(_)) => {} + _ => return Ok(()), + } + + let mut reference_args = 0; + for arg in &efn.args { + if let Type::Ref(_) = &arg.ty { + reference_args += 1; + } + } + + if reference_args == 1 { + Ok(()) + } else { + Err(Error::new_spanned( + efn, + "functions that return a reference must take exactly one input reference", + )) + } +} + +fn describe(ty: &Type, types: &Types) -> String { + match ty { + Type::Ident(ident) => { + if types.structs.contains_key(ident) { + "struct".to_owned() + } else if types.cxx.contains(ident) { + "C++ type".to_owned() + } else if types.rust.contains(ident) { + "opaque Rust type".to_owned() + } else if Atom::from(ident) == Some(CxxString) { + "C++ string".to_owned() + } else { + ident.to_string() + } + } + Type::RustBox(_) => "Box".to_owned(), + Type::UniquePtr(_) => "unique_ptr".to_owned(), + Type::Ref(_) => "reference".to_owned(), + Type::Str(_) => "&str".to_owned(), + } +} + +fn unsupported_type(ident: &Ident) -> Error { + Error::new(ident.span(), "unsupported type") +} + +fn unsupported_cxx_type_in_box(unique_ptr: &Ty1) -> Error { + Error::new_spanned(unique_ptr, error::BOX_CXX_TYPE.msg) +} + +fn unsupported_box_target(unique_ptr: &Ty1) -> Error { + Error::new_spanned(unique_ptr, "unsupported target type of Box") +} + +fn unsupported_rust_type_in_unique_ptr(unique_ptr: &Ty1) -> Error { + Error::new_spanned(unique_ptr, "unique_ptr of a Rust type is not supported yet") +} + +fn unsupported_unique_ptr_target(unique_ptr: &Ty1) -> Error { + Error::new_spanned(unique_ptr, "unsupported unique_ptr target type") +} + +fn field_by_value(field: &Var, types: &Types) -> Error { + let desc = describe(&field.ty, types); + let message = format!("using {} by value is not supported", desc); + Error::new_spanned(field, message) +} + +fn argument_by_value(arg: &Var, types: &Types) -> Error { + let desc = describe(&arg.ty, types); + let message = format!("passing {} by value is not supported", desc); + Error::new_spanned(arg, message) +} + +fn return_by_value(ty: &Type, types: &Types) -> Error { + let desc = describe(ty, types); + let message = format!("returning {} by value is not supported", desc); + Error::new_spanned(ty, message) +} diff --git a/syntax/doc.rs b/syntax/doc.rs new file mode 100644 index 00000000..60bb4da1 --- /dev/null +++ b/syntax/doc.rs @@ -0,0 +1,37 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::LitStr; + +pub struct Doc { + fragments: Vec, +} + +impl Doc { + pub fn new() -> Self { + Doc { + fragments: Vec::new(), + } + } + + pub fn push(&mut self, lit: LitStr) { + self.fragments.push(lit); + } + + pub fn to_string(&self) -> String { + let mut doc = String::new(); + for lit in &self.fragments { + doc += &lit.value(); + doc.push('\n'); + } + doc + } +} + +impl ToTokens for Doc { + fn to_tokens(&self, tokens: &mut TokenStream) { + let fragments = &self.fragments; + tokens.extend(quote! { + #(#[doc = #fragments])* + }); + } +} diff --git a/syntax/error.rs b/syntax/error.rs new file mode 100644 index 00000000..f52d6515 --- /dev/null +++ b/syntax/error.rs @@ -0,0 +1,70 @@ +use std::fmt::{self, Display}; + +#[derive(Copy, Clone)] +pub struct Error { + pub msg: &'static str, + pub label: Option<&'static str>, + pub note: Option<&'static str>, +} + +impl Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.msg.fmt(formatter) + } +} + +pub static ERRORS: &[Error] = &[ + BOX_CXX_TYPE, + CXXBRIDGE_RESERVED, + CXX_STRING_BY_VALUE, + CXX_TYPE_BY_VALUE, + DOUBLE_UNDERSCORE, + RUST_TYPE_BY_VALUE, + USE_NOT_ALLOWED, +]; + +pub static BOX_CXX_TYPE: Error = Error { + msg: "Box of a C++ type is not supported yet", + label: None, + note: Some("hint: use UniquePtr<>"), +}; + +pub static CXXBRIDGE_RESERVED: Error = Error { + msg: "identifiers starting with cxxbridge are reserved", + label: Some("reserved identifier"), + note: Some("identifiers starting with cxxbridge are reserved"), +}; + +pub static CXX_STRING_BY_VALUE: Error = Error { + msg: "C++ string by value is not supported", + label: None, + note: Some("hint: wrap it in a UniquePtr<>"), +}; + +pub static CXX_TYPE_BY_VALUE: Error = Error { + msg: "C++ type by value is not supported", + label: None, + note: Some("hint: wrap it in a Box<> or UniquePtr<>"), +}; + +pub static DOUBLE_UNDERSCORE: Error = Error { + msg: "identifiers containing double underscore are reserved in C++", + label: Some("reserved identifier"), + note: Some("identifiers containing double underscore are reserved in C++"), +}; + +pub static RUST_TYPE_BY_VALUE: Error = Error { + msg: "opaque Rust type by value is not supported", + label: None, + note: Some("hint: wrap it in a Box<>"), +}; + +pub static USE_NOT_ALLOWED: Error = Error { + msg: "`use` items are not allowed within cxx bridge", + label: Some("not allowed"), + note: Some( + "`use` items are not allowed within cxx bridge; only types defined\n\ + within your bridge, primitive types, or types exported by the cxx\n\ + crate may be used", + ), +}; diff --git a/syntax/ident.rs b/syntax/ident.rs new file mode 100644 index 00000000..84be4cb3 --- /dev/null +++ b/syntax/ident.rs @@ -0,0 +1,37 @@ +use crate::syntax::{error, Api}; +use proc_macro2::Ident; +use syn::{Error, Result}; + +pub(crate) fn check(ident: &Ident) -> Result<()> { + let s = ident.to_string(); + if s.contains("__") { + Err(Error::new(ident.span(), error::DOUBLE_UNDERSCORE.msg)) + } else if s.starts_with("cxxbridge") { + Err(Error::new(ident.span(), error::CXXBRIDGE_RESERVED.msg)) + } else { + Ok(()) + } +} + +pub(crate) fn check_all(apis: &[Api], errors: &mut Vec) { + for api in apis { + match api { + Api::Include(_) => {} + Api::Struct(strct) => { + errors.extend(check(&strct.ident).err()); + for field in &strct.fields { + errors.extend(check(&field.ident).err()); + } + } + Api::CxxType(ety) | Api::RustType(ety) => { + errors.extend(check(&ety.ident).err()); + } + Api::CxxFunction(efn) | Api::RustFunction(efn) => { + errors.extend(check(&efn.ident).err()); + for arg in &efn.args { + errors.extend(check(&arg.ident).err()); + } + } + } + } +} diff --git a/syntax/impls.rs b/syntax/impls.rs new file mode 100644 index 00000000..8153f03c --- /dev/null +++ b/syntax/impls.rs @@ -0,0 +1,31 @@ +use crate::syntax::{Ref, Ty1}; +use std::hash::{Hash, Hasher}; + +impl Eq for Ty1 {} + +impl PartialEq for Ty1 { + fn eq(&self, other: &Ty1) -> bool { + self.name == other.name && self.inner == other.inner + } +} + +impl Hash for Ty1 { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.inner.hash(state); + } +} + +impl Eq for Ref {} + +impl PartialEq for Ref { + fn eq(&self, other: &Ref) -> bool { + self.inner == other.inner + } +} + +impl Hash for Ref { + fn hash(&self, state: &mut H) { + self.inner.hash(state); + } +} diff --git a/syntax/mod.rs b/syntax/mod.rs new file mode 100644 index 00000000..c5cc1de7 --- /dev/null +++ b/syntax/mod.rs @@ -0,0 +1,92 @@ +// Functionality that is shared between the cxxbridge macro and the cmd. + +pub mod atom; +mod attrs; +pub mod check; +mod doc; +pub mod error; +pub mod ident; +mod impls; +mod parse; +pub mod set; +mod tokens; +pub mod types; + +use proc_macro2::Ident; +use syn::{LitStr, Token}; + +pub use self::atom::Atom; +pub use self::doc::Doc; +pub use self::parse::parse_items; +pub use self::types::Types; + +pub enum Api { + Include(LitStr), + Struct(Struct), + CxxType(ExternType), + CxxFunction(ExternFn), + RustType(ExternType), + RustFunction(ExternFn), +} + +pub struct ExternType { + pub doc: Doc, + pub type_token: Token![type], + pub ident: Ident, +} + +pub struct Struct { + pub doc: Doc, + pub derives: Vec, + pub struct_token: Token![struct], + pub ident: Ident, + pub fields: Vec, +} + +pub struct ExternFn { + pub doc: Doc, + pub fn_token: Token![fn], + pub ident: Ident, + pub receiver: Option, + pub args: Vec, + pub ret: Option, + pub semi_token: Token![;], +} + +pub struct Var { + pub ident: Ident, + pub ty: Type, +} + +pub struct Receiver { + pub mutability: Option, + pub ident: Ident, +} + +#[derive(Hash, Eq, PartialEq)] +pub enum Type { + Ident(Ident), + RustBox(Box), + UniquePtr(Box), + Ref(Box), + Str(Box), +} + +pub struct Ty1 { + pub name: Ident, + pub langle: Token![<], + pub inner: Type, + pub rangle: Token![>], +} + +pub struct Ref { + pub ampersand: Token![&], + pub mutability: Option, + pub inner: Type, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Derive { + Clone, + Copy, +} diff --git a/syntax/parse.rs b/syntax/parse.rs new file mode 100644 index 00000000..efe6ff8d --- /dev/null +++ b/syntax/parse.rs @@ -0,0 +1,265 @@ +use crate::syntax::{ + attrs, error, Api, Atom, Doc, ExternFn, ExternType, Receiver, Ref, Struct, Ty1, Type, Var, +}; +use proc_macro2::Ident; +use quote::quote; +use syn::{ + Abi, Error, Fields, FnArg, ForeignItem, ForeignItemFn, ForeignItemType, GenericArgument, Item, + ItemForeignMod, ItemStruct, Pat, PathArguments, Result, ReturnType, Type as RustType, +}; + +#[derive(Copy, Clone)] +enum Lang { + Cxx, + Rust, +} + +pub fn parse_items(items: Vec) -> Result> { + let mut apis = Vec::new(); + for item in items { + match item { + Item::Struct(item) => { + let strct = parse_struct(item)?; + apis.push(strct); + } + Item::ForeignMod(foreign_mod) => { + let functions = parse_foreign_mod(foreign_mod)?; + apis.extend(functions); + } + Item::Use(item) => return Err(Error::new_spanned(item, error::USE_NOT_ALLOWED)), + _ => return Err(Error::new_spanned(item, "unsupported item")), + } + } + Ok(apis) +} + +fn parse_struct(item: ItemStruct) -> Result { + let generics = &item.generics; + if !generics.params.is_empty() || generics.where_clause.is_some() { + let struct_token = item.struct_token; + let ident = &item.ident; + let where_clause = &generics.where_clause; + let span = quote!(#struct_token #ident #generics #where_clause); + return Err(Error::new_spanned( + span, + "struct with generic parameters is not supported yet", + )); + } + + let mut doc = Doc::new(); + let mut derives = Vec::new(); + attrs::parse(&item.attrs, &mut doc, Some(&mut derives))?; + check_reserved_name(&item.ident)?; + match item.fields { + Fields::Named(fields) => Ok(Api::Struct(Struct { + doc, + derives, + struct_token: item.struct_token, + ident: item.ident, + fields: fields + .named + .into_iter() + .map(|field| { + Ok(Var { + ident: field.ident.unwrap(), + ty: parse_type(&field.ty)?, + }) + }) + .collect::>()?, + })), + Fields::Unit => Err(Error::new_spanned(item, "unit structs are not supported")), + Fields::Unnamed(_) => Err(Error::new_spanned(item, "tuple structs are not supported")), + } +} + +fn parse_foreign_mod(foreign_mod: ItemForeignMod) -> Result> { + let lang = parse_lang(foreign_mod.abi)?; + let api_type = match lang { + Lang::Cxx => Api::CxxType, + Lang::Rust => Api::RustType, + }; + let api_function = match lang { + Lang::Cxx => Api::CxxFunction, + Lang::Rust => Api::RustFunction, + }; + + let mut items = Vec::new(); + for foreign in &foreign_mod.items { + match foreign { + ForeignItem::Type(foreign) => { + check_reserved_name(&foreign.ident)?; + let ety = parse_extern_type(foreign)?; + items.push(api_type(ety)); + } + ForeignItem::Fn(foreign) => { + let efn = parse_extern_fn(foreign)?; + items.push(api_function(efn)); + } + ForeignItem::Macro(foreign) if foreign.mac.path.is_ident("include") => { + let include = foreign.mac.parse_body()?; + items.push(Api::Include(include)); + } + _ => return Err(Error::new_spanned(foreign, "unsupported foreign item")), + } + } + Ok(items) +} + +fn parse_lang(abi: Abi) -> Result { + let name = match &abi.name { + Some(name) => name, + None => { + return Err(Error::new_spanned( + abi, + "ABI name is required, extern \"C\" or extern \"Rust\"", + )); + } + }; + match name.value().as_str() { + "C" => Ok(Lang::Cxx), + "Rust" => Ok(Lang::Rust), + _ => Err(Error::new_spanned(abi, "unrecognized ABI")), + } +} + +fn parse_extern_type(foreign_type: &ForeignItemType) -> Result { + let doc = attrs::parse_doc(&foreign_type.attrs)?; + let type_token = foreign_type.type_token; + let ident = foreign_type.ident.clone(); + Ok(ExternType { + doc, + type_token, + ident, + }) +} + +fn parse_extern_fn(foreign_fn: &ForeignItemFn) -> Result { + let generics = &foreign_fn.sig.generics; + if !generics.params.is_empty() || generics.where_clause.is_some() { + return Err(Error::new_spanned( + foreign_fn, + "extern function with generic parameters is not supported yet", + )); + } + if let Some(variadic) = &foreign_fn.sig.variadic { + return Err(Error::new_spanned( + variadic, + "variadic function is not supported yet", + )); + } + + let mut receiver = None; + let mut args = Vec::new(); + for arg in &foreign_fn.sig.inputs { + match arg { + FnArg::Receiver(receiver) => { + return Err(Error::new_spanned(receiver, "unsupported signature")) + } + FnArg::Typed(arg) => { + let ident = match arg.pat.as_ref() { + Pat::Ident(pat) => pat.ident.clone(), + _ => return Err(Error::new_spanned(arg, "unsupported signature")), + }; + let ty = parse_type(&arg.ty)?; + if ident != "self" { + args.push(Var { ident, ty }); + continue; + } + if let Type::Ref(reference) = ty { + if let Type::Ident(ident) = reference.inner { + receiver = Some(Receiver { + mutability: reference.mutability, + ident, + }); + continue; + } + } + return Err(Error::new_spanned(arg, "unsupported method receiver")); + } + } + } + let ret = match &foreign_fn.sig.output { + ReturnType::Default => None, + ReturnType::Type(_, ty) => Some(parse_type(ty)?), + }; + let doc = attrs::parse_doc(&foreign_fn.attrs)?; + let fn_token = foreign_fn.sig.fn_token; + let ident = foreign_fn.sig.ident.clone(); + let semi_token = foreign_fn.semi_token; + Ok(ExternFn { + doc, + fn_token, + ident, + receiver, + args, + ret, + semi_token, + }) +} + +fn parse_type(ty: &RustType) -> Result { + match &ty { + RustType::Reference(ty) => { + let inner = parse_type(&ty.elem)?; + let which = match &inner { + Type::Ident(ident) if ident == "str" => { + if ty.mutability.is_some() { + return Err(Error::new_spanned(ty, "unsupported type")); + } else { + Type::Str + } + } + _ => Type::Ref, + }; + return Ok(which(Box::new(Ref { + ampersand: ty.and_token, + mutability: ty.mutability, + inner, + }))); + } + RustType::Path(ty) => { + let path = &ty.path; + if ty.qself.is_none() && path.leading_colon.is_none() && path.segments.len() == 1 { + let segment = &path.segments[0]; + let ident = segment.ident.clone(); + match &segment.arguments { + PathArguments::None => return Ok(Type::Ident(ident)), + PathArguments::AngleBracketed(generic) => { + if ident == "UniquePtr" && generic.args.len() == 1 { + if let GenericArgument::Type(arg) = &generic.args[0] { + let inner = parse_type(arg)?; + return Ok(Type::UniquePtr(Box::new(Ty1 { + name: ident, + langle: generic.lt_token, + inner, + rangle: generic.gt_token, + }))); + } + } else if ident == "Box" && generic.args.len() == 1 { + if let GenericArgument::Type(arg) = &generic.args[0] { + let inner = parse_type(arg)?; + return Ok(Type::RustBox(Box::new(Ty1 { + name: ident, + langle: generic.lt_token, + inner, + rangle: generic.gt_token, + }))); + } + } + } + PathArguments::Parenthesized(_) => {} + } + } + } + _ => {} + } + Err(Error::new_spanned(ty, "unsupported type")) +} + +fn check_reserved_name(ident: &Ident) -> Result<()> { + if ident == "Box" || ident == "UniquePtr" || Atom::from(ident).is_some() { + Err(Error::new(ident.span(), "reserved name")) + } else { + Ok(()) + } +} diff --git a/syntax/set.rs b/syntax/set.rs new file mode 100644 index 00000000..ca816cff --- /dev/null +++ b/syntax/set.rs @@ -0,0 +1,47 @@ +use std::collections::HashSet; +use std::hash::Hash; +use std::slice; + +pub struct OrderedSet<'a, T> { + set: HashSet<&'a T>, + vec: Vec<&'a T>, +} + +impl<'a, T> OrderedSet<'a, T> +where + T: Hash + Eq, +{ + pub fn new() -> Self { + OrderedSet { + set: HashSet::new(), + vec: Vec::new(), + } + } + + pub fn insert(&mut self, value: &'a T) { + if self.set.insert(value) { + self.vec.push(value); + } + } + + pub fn contains(&self, value: &T) -> bool { + self.set.contains(value) + } +} + +impl<'s, 'a, T> IntoIterator for &'s OrderedSet<'a, T> { + type Item = &'a T; + type IntoIter = Iter<'s, 'a, T>; + fn into_iter(self) -> Self::IntoIter { + Iter(self.vec.iter()) + } +} + +pub struct Iter<'s, 'a, T>(slice::Iter<'s, &'a T>); + +impl<'s, 'a, T> Iterator for Iter<'s, 'a, T> { + type Item = &'a T; + fn next(&mut self) -> Option { + self.0.next().copied() + } +} diff --git a/syntax/tokens.rs b/syntax/tokens.rs new file mode 100644 index 00000000..f553207d --- /dev/null +++ b/syntax/tokens.rs @@ -0,0 +1,67 @@ +use crate::syntax::{Derive, ExternFn, Ref, Ty1, Type, Var}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote_spanned, ToTokens}; +use syn::Token; + +impl ToTokens for Type { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Type::Ident(ident) => { + if ident == "CxxString" { + let span = ident.span(); + tokens.extend(quote_spanned!(span=> ::cxx::)); + } + ident.to_tokens(tokens); + } + Type::RustBox(ty) | Type::UniquePtr(ty) => ty.to_tokens(tokens), + Type::Ref(r) | Type::Str(r) => r.to_tokens(tokens), + } + } +} + +impl ToTokens for Var { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.ident.to_tokens(tokens); + Token![:](self.ident.span()).to_tokens(tokens); + self.ty.to_tokens(tokens); + } +} + +impl ToTokens for Ty1 { + fn to_tokens(&self, tokens: &mut TokenStream) { + if self.name == "UniquePtr" { + let span = self.name.span(); + tokens.extend(quote_spanned!(span=> ::cxx::)); + } + self.name.to_tokens(tokens); + self.langle.to_tokens(tokens); + self.inner.to_tokens(tokens); + self.rangle.to_tokens(tokens); + } +} + +impl ToTokens for Ref { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.ampersand.to_tokens(tokens); + self.mutability.to_tokens(tokens); + self.inner.to_tokens(tokens); + } +} + +impl ToTokens for Derive { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = match self { + Derive::Clone => "Clone", + Derive::Copy => "Copy", + }; + Ident::new(name, Span::call_site()).to_tokens(tokens); + } +} + +impl ToTokens for ExternFn { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.fn_token.to_tokens(tokens); + self.ident.to_tokens(tokens); + self.semi_token.to_tokens(tokens); + } +} diff --git a/syntax/types.rs b/syntax/types.rs new file mode 100644 index 00000000..50f13b19 --- /dev/null +++ b/syntax/types.rs @@ -0,0 +1,121 @@ +use crate::syntax::atom::Atom::{self, *}; +use crate::syntax::set::OrderedSet as Set; +use crate::syntax::{Api, Derive, ExternType, Struct, Type}; +use proc_macro2::Ident; +use quote::quote; +use std::collections::BTreeMap as Map; +use syn::{Error, Result}; + +pub struct Types<'a> { + pub all: Set<'a, Type>, + pub structs: Map, + pub cxx: Set<'a, Ident>, + pub rust: Set<'a, Ident>, +} + +impl<'a> Types<'a> { + pub fn collect(apis: &'a [Api]) -> Result { + let mut all = Set::new(); + let mut structs = Map::new(); + let mut cxx = Set::new(); + let mut rust = Set::new(); + + fn visit<'a>(all: &mut Set<'a, Type>, ty: &'a Type) { + all.insert(ty); + match ty { + Type::Ident(_) | Type::Str(_) => {} + Type::RustBox(ty) | Type::UniquePtr(ty) => visit(all, &ty.inner), + Type::Ref(r) => visit(all, &r.inner), + } + } + + for api in apis { + match api { + Api::Include(_) => {} + Api::Struct(strct) => { + let ident = &strct.ident; + if structs.contains_key(ident) || cxx.contains(ident) || rust.contains(ident) { + return Err(duplicate_struct(strct)); + } + structs.insert(strct.ident.clone(), strct); + for field in &strct.fields { + visit(&mut all, &field.ty); + } + } + Api::CxxType(ety) => { + let ident = &ety.ident; + if structs.contains_key(ident) || cxx.contains(ident) || rust.contains(ident) { + return Err(duplicate_type(ety)); + } + cxx.insert(ident); + } + Api::RustType(ety) => { + let ident = &ety.ident; + if structs.contains_key(ident) || cxx.contains(ident) || rust.contains(ident) { + return Err(duplicate_type(ety)); + } + rust.insert(ident); + } + Api::CxxFunction(efn) | Api::RustFunction(efn) => { + for arg in &efn.args { + visit(&mut all, &arg.ty); + } + if let Some(ret) = &efn.ret { + visit(&mut all, ret); + } + } + } + } + + Ok(Types { + all, + structs, + cxx, + rust, + }) + } + + pub fn needs_indirect_abi(&self, ty: &Type) -> bool { + match ty { + Type::Ident(ident) => { + if let Some(strct) = self.structs.get(ident) { + !self.is_pod(strct) + } else { + Atom::from(ident) == Some(RustString) + } + } + _ => false, + } + } + + pub fn is_pod(&self, strct: &Struct) -> bool { + for derive in &strct.derives { + if *derive == Derive::Copy { + return true; + } + } + false + } +} + +impl<'t, 'a> IntoIterator for &'t Types<'a> { + type Item = &'a Type; + type IntoIter = crate::syntax::set::Iter<'t, 'a, Type>; + fn into_iter(self) -> Self::IntoIter { + self.all.into_iter() + } +} + +fn duplicate_struct(strct: &Struct) -> Error { + let struct_token = strct.struct_token; + let ident = &strct.ident; + let range = quote!(#struct_token #ident); + Error::new_spanned(range, "duplicate type") +} + +fn duplicate_type(ety: &ExternType) -> Error { + let type_token = ety.type_token; + let ident = &ety.ident; + let range = quote!(#type_token #ident); + Error::new_spanned(range, "duplicate type") +} diff --git a/tests/compiletest.rs b/tests/compiletest.rs new file mode 100644 index 00000000..f9aea23b --- /dev/null +++ b/tests/compiletest.rs @@ -0,0 +1,6 @@ +#[rustversion::attr(not(nightly), ignore)] +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/tests/ui/opaque_not_sized.rs b/tests/ui/opaque_not_sized.rs new file mode 100644 index 00000000..754e8d36 --- /dev/null +++ b/tests/ui/opaque_not_sized.rs @@ -0,0 +1,10 @@ +#[cxx::bridge] +mod ffi { + extern "Rust" { + type TypeR; + } +} + +struct TypeR(str); + +fn main() {} diff --git a/tests/ui/opaque_not_sized.stderr b/tests/ui/opaque_not_sized.stderr new file mode 100644 index 00000000..419b83da --- /dev/null +++ b/tests/ui/opaque_not_sized.stderr @@ -0,0 +1,12 @@ +error[E0277]: the size for values of type `str` cannot be known at compilation time + --> $DIR/opaque_not_sized.rs:1:1 + | +1 | #[cxx::bridge] + | ^^^^^^^^^^^^^^ + | | + | doesn't have a size known at compile-time + | required by this bound in `ffi::_::__assert_sized` + | + = help: within `TypeR`, the trait `std::marker::Sized` is not implemented for `str` + = note: to learn more, visit + = note: required because it appears within the type `TypeR`