diff --git a/.plzconfig b/.plzconfig index 353276b..903415c 100644 --- a/.plzconfig +++ b/.plzconfig @@ -15,6 +15,7 @@ PleaseRustTool = //tools/please_rust:bootstrap ; names cannot collide with a consumer's third_party/rust ones CriterionDep = //third_party/crates:criterion BindgenTool = ///third_party/crates/bindgen_cli//:bindgen_bin +CbindgenTool = ///third_party/crates/cbindgen//:cbindgen_bin Rustc = //third_party/rust:toolchain_rustc|rustc ; Exercise the pipelined shape in this repo; consumers default to off PipelinedCompilation = true @@ -129,6 +130,12 @@ DefaultValue = ///third_party/crates/bindgen_cli//:bindgen_bin Help = Build label of the bindgen binary used by rust_bindgen. Built from crates via rust_repo (bindgen-cli). Inherit = true +[PluginConfig "cbindgen_tool"] +ConfigKey = CbindgenTool +DefaultValue = ///third_party/crates/cbindgen//:cbindgen_bin +Help = Build label of the cbindgen binary used by rust_cbindgen. Built from crates via rust_repo (cbindgen), the same way bindgen is. +Inherit = true + [PluginConfig "libclang_path"] ConfigKey = LibclangPath DefaultValue = diff --git a/README.md b/README.md index 1467698..653fa5f 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,26 @@ rust_library( ) ``` +The reverse, a C header generated from Rust so C can call into a +`staticlib` or `cdylib`, comes from `rust_cbindgen`. cbindgen is declared the +same way (`lock --add cbindgen`), and parses the source rather than compiling +it, so the rule needs no toolchain: +```python +rust_cbindgen( + name = "ffi_header", # generates ffi_header.h + root = "src/lib.rs", # .hpp with lang = "c++" +) + +c_binary( + name = "uses_rust", + srcs = ["main.c"], + hdrs = [":ffi_header"], + deps = [":rust_ffi"], +) +``` +A signature that changes on the Rust side then stops the C compiling, rather +than compiling and crashing. + Protobuf and gRPC codegen live in [rust-proto-rules](https://github.com/becomeliminal/rust-proto-rules), a separate plugin that pins these rules by tag and plugs into the proto diff --git a/build_defs/rust.build_defs b/build_defs/rust.build_defs index 066e759..1ab47e9 100644 --- a/build_defs/rust.build_defs +++ b/build_defs/rust.build_defs @@ -1220,6 +1220,56 @@ def rust_bindgen(name:str, header:str, srcs:list=[], clang_args:list=[], bindgen ) +def rust_cbindgen(name:str, root:str, srcs:list=[], lang:str="c", config:str="", crate_name:str="", + cbindgen_flags:list=[], visibility:list=None): + """Generates a C or C++ header from Rust, the reverse of rust_bindgen. + + C code calling into a `staticlib` or `cdylib` needs declarations for what + it is calling. Hand-writing them means nothing checks them against the + Rust definitions, and a signature that changes on one side goes on + compiling on the other until it crashes. + + The output is a single header, usable as a `hdrs` entry of a c_library or + included directly by a c_binary's sources. + + Args: + name (str): Name of the rule; the output is .h, or .hpp for C++. + root (str): The crate's root module, the same file a rust_library + would take as its root. + srcs (list): The rest of the crate's sources, when the root declares + modules. cbindgen follows `mod` from the root, so a + crate of several files needs them staged. + lang (str): Output language: 'c', 'c++' or 'cython'. + config (str): A cbindgen.toml, for anything the flags do not cover. + crate_name (str): Crate name recorded in the header's include guard. + Defaults to the rule name. + cbindgen_flags (list): Extra flags passed to cbindgen. + visibility (list): Visibility declaration. + """ + ext = "hpp" if lang == "c++" else ("pyx" if lang == "cython" else "h") + flags = " ".join(cbindgen_flags) + cfg = " --config $SRCS_CONFIG" if config else "" + crate_flag = f" --crate {crate_name}" if crate_name else "" + return build_rule( + name = name, + srcs = { + "root": [root], + "mods": srcs, + "config": [config] if config else [], + }, + outs = [f"{name}.{ext}"], + # cbindgen parses the source rather than compiling it, so it needs no + # toolchain and no dependencies: a crate's public extern "C" surface + # is decided by its own text. + cmd = f"$TOOLS_CBINDGEN --lang {lang}{cfg}{crate_flag} -o $OUT {flags} $SRCS_ROOT", + tools = { + "cbindgen": [CONFIG.RUST.CBINDGEN_TOOL], + }, + visibility = visibility, + labels = ["rust", "codegen"], + ) + + def rust_crate_download(name:str, crate:str, version:str, hashes:list=None, labels:list=[], visibility:list=None): """Downloads a crate from crates.io. diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index e69c033..6aeb0ef 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -49,7 +49,6 @@ rather than by release: - **Missing capability:** [#23](https://github.com/becomeliminal/rust-rules/issues/23) cross-compiling C, [#24](https://github.com/becomeliminal/rust-rules/issues/24) channels, - [#25](https://github.com/becomeliminal/rust-rules/issues/25) cbindgen, [#26](https://github.com/becomeliminal/rust-rules/issues/26) wasm-bindgen, [#27](https://github.com/becomeliminal/rust-rules/issues/27) multi-platform locks, [#28](https://github.com/becomeliminal/rust-rules/issues/28) bench profile, @@ -120,7 +119,7 @@ rather than by release: | **Documentation**
rustdoc HTML | **yes**. rust_doc | **yes**. rust_doc | **yes**. cargo doc | | **Coverage**
Line coverage from instrumented tests | **yes**. -C instrument-coverage into plz cover | **yes**. Supported | **partial**. External tooling, llvm-cov | | **C header bindings**
bindgen | **yes**. rust_bindgen, tool built from declared crates | **yes**. rust_bindgen | **partial**. build.rs calling bindgen | -| **Rust to C headers**
cbindgen | **no**. Open | **yes**. Supported | **partial**. build.rs calling cbindgen | +| **Rust to C headers**
cbindgen | **yes**. rust_cbindgen, tool built from declared crates | **no**. No cbindgen rule or extension | **partial**. cbindgen called from build.rs | | **wasm-bindgen**
JS bindings | **no**. Groundwork only | **yes**. Supported | **partial**. External tool | | **Publish to crates.io**
cargo publish | **no**. Deliberate non-goal | **no**. Not its job | **yes**. Native | diff --git a/test/cc_interop/BUILD b/test/cc_interop/BUILD index e50928d..9bca75f 100644 --- a/test/cc_interop/BUILD +++ b/test/cc_interop/BUILD @@ -35,8 +35,18 @@ rust_library( edition = "2021", ) +# The header the C side includes is generated from the Rust source rather +# than hand-written, so a signature that changes on one side stops the other +# compiling instead of crashing at run time. +rust_cbindgen( + name = "ffi_header", + root = "rust_ffi_lib.rs", + crate_name = "ffi", +) + c_binary( name = "uses_rust", srcs = ["main.c"], + hdrs = [":ffi_header"], deps = [":rust_ffi"], ) diff --git a/test/cc_interop/main.c b/test/cc_interop/main.c index a50784e..e55c2b0 100644 --- a/test/cc_interop/main.c +++ b/test/cc_interop/main.c @@ -1,5 +1,7 @@ #include -int ffi_add(int a, int b); +// Generated by rust_cbindgen from rust_ffi_lib.rs. Declaring ffi_add by hand +// here is what this is replacing: nothing checked it against the Rust. +#include "ffi_header.h" int main(void) { printf("rust says %d\n", ffi_add(40, 2)); return 0; diff --git a/third_party/crates/BUILD b/third_party/crates/BUILD index b280f44..4e812a6 100644 --- a/third_party/crates/BUILD +++ b/third_party/crates/BUILD @@ -141,7 +141,7 @@ rust_repo( ) rust_repo( - name = "itertools", + name = "itertools-0.12.1", crate = "itertools", version = "0.12.1", indirect = True, @@ -266,6 +266,7 @@ rust_repo( crate = "linux-raw-sys", version = "0.4.14", indirect = True, + platforms = ["linux"], ) @@ -347,7 +348,7 @@ rust_repo( ) rust_repo( - name = "heck", + name = "heck-0.5.0", crate = "heck", version = "0.5.0", hashes = ["2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"], @@ -362,9 +363,10 @@ rust_repo( ) rust_repo( - name = "log-0.4.21", + name = "log", crate = "log", - version = "0.4.21", + version = "0.4.33", + hashes = ["0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"], indirect = True, ) @@ -723,17 +725,19 @@ rust_repo( ) rust_repo( - name = "libsecp256k1_gen_ecmult", + name = "libsecp256k1_gen_ecmult-0.3.0", crate = "libsecp256k1-gen-ecmult", version = "0.3.0", indirect = True, + platforms = [], ) rust_repo( - name = "libsecp256k1_gen_genmult", + name = "libsecp256k1_gen_genmult-0.3.0", crate = "libsecp256k1-gen-genmult", version = "0.3.0", indirect = True, + platforms = [], ) rust_repo( @@ -1081,7 +1085,7 @@ rust_repo( ) rust_repo( - name = "serde_spanned", + name = "serde_spanned-0.6.9", crate = "serde_spanned", version = "0.6.9", hashes = ["bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"], @@ -1137,7 +1141,7 @@ rust_repo( ) rust_repo( - name = "toml_datetime", + name = "toml_datetime-0.6.11", crate = "toml_datetime", version = "0.6.11", hashes = ["22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"], @@ -1180,7 +1184,7 @@ rust_repo( rust_repo( - name = "winnow", + name = "winnow-0.7.14", crate = "winnow", version = "0.7.14", hashes = ["5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"], @@ -1221,7 +1225,7 @@ rust_resolve( "either|either|1.12.0||false|true", "lock_api|lock_api|0.4.12||false|true", "parking_lot|parking_lot|0.12.3||false|true", - "itertools|itertools|0.12.1||false|true", + "itertools-0.12.1|itertools|0.12.1||false|true", "slab|slab|0.4.9||false|true", "pin_utils|pin-utils|0.1.0||false|true", "fnv|fnv|1.0.7||false|true", @@ -1250,9 +1254,9 @@ rust_resolve( "want|want|0.3.1||false|true", "petgraph|petgraph|0.6.5||false|true", "futures_channel|futures-channel|0.3.30||false|true", - "heck|heck|0.5.0||false|true", + "heck-0.5.0|heck|0.5.0||false|true", "httpdate|httpdate|1.0.3||false|true", - "log-0.4.21|log|0.4.21||false|true", + "log|log|0.4.33||false|true", "multimap|multimap|0.10.0||false|true", "tower_layer|tower-layer|0.3.2||false|true", "regex_automata|regex-automata|0.4.7||false|true", @@ -1302,8 +1306,8 @@ rust_resolve( "generic_array|generic-array|0.14.7||false|true", "version_check|version_check|0.9.5||false|true", "typenum|typenum|1.17.0||false|true", - "libsecp256k1_gen_ecmult|libsecp256k1-gen-ecmult|0.3.0||false|true", - "libsecp256k1_gen_genmult|libsecp256k1-gen-genmult|0.3.0||false|true", + "libsecp256k1_gen_ecmult-0.3.0|libsecp256k1-gen-ecmult|0.3.0||false|true", + "libsecp256k1_gen_genmult-0.3.0|libsecp256k1-gen-genmult|0.3.0||false|true", "blake3|blake3|1.5.4|default,std|true|true", "arrayvec|arrayvec|0.7.6||false|true", "constant_time_eq|constant_time_eq|0.3.1||false|true", @@ -1348,19 +1352,19 @@ rust_resolve( "serde_core|serde_core|1.0.228||false|true", "serde_derive|serde_derive|1.0.228||false|true", "serde_json|serde_json|1.0.147|default|true|true", - "serde_spanned|serde_spanned|0.6.9||false|true", + "serde_spanned-0.6.9|serde_spanned|0.6.9||false|true", "smallvec|smallvec|1.15.2||false|true", "strsim|strsim|0.11.1||false|true", "syn-2.0.111|syn|2.0.111|full,parsing,default|true|true", "thiserror-1.0.69|thiserror|1.0.69|default|true|true", "thiserror_impl-1.0.69|thiserror-impl|1.0.69||false|true", "toml|toml|0.8.23|default|true|true", - "toml_datetime|toml_datetime|0.6.11||false|true", + "toml_datetime-0.6.11|toml_datetime|0.6.11||false|true", "toml_edit|toml_edit|0.22.27||false|true", "toml_write|toml_write|0.1.2||false|true", "unicode_ident|unicode-ident|1.0.22||false|true", "utf8parse|utf8parse|0.2.2||false|true", - "winnow|winnow|0.7.14||false|true", + "winnow-0.7.14|winnow|0.7.14||false|true", "zmij|zmij|0.1.9||false|true", "bindgen|bindgen|0.71.1||false|true", "bindgen_cli|bindgen-cli|0.71.1||true|true", @@ -1379,7 +1383,7 @@ rust_resolve( "libloading|libloading|0.8.9||true|true", "annotate_snippets|annotate-snippets|0.11.5||true|true", "unicode_width|unicode-width|0.2.2||false|true", - "log|log|0.4.33||false|true", + "log-0.4.33|log|0.4.33||false|true", "priority_queue|priority-queue|2.7.0||false|true", "pubgrub|pubgrub|0.4.0||true|true", "syn|syn|3.0.3||true|true", @@ -1388,14 +1392,23 @@ rust_resolve( "version_ranges|version-ranges|0.1.3||false|true", "base64-0.12.3|base64|0.12.3||true|true", "getrandom-0.1.16|getrandom|0.1.16||false|true", - "heck-0.4.1|heck|0.4.1||true|true", - "itertools-0.10.5|itertools|0.10.5||true|true", - "libsecp256k1_gen_ecmult-0.2.1|libsecp256k1-gen-ecmult|0.2.1||true|true", - "libsecp256k1_gen_genmult-0.2.1|libsecp256k1-gen-genmult|0.2.1||true|true", + "heck|heck|0.4.1||true|true", + "itertools|itertools|0.10.5||true|true", + "libsecp256k1_gen_ecmult|libsecp256k1-gen-ecmult|0.2.1||true|true", + "libsecp256k1_gen_genmult|libsecp256k1-gen-genmult|0.2.1||true|true", "rand-0.7.3|rand|0.7.3||true|true", "rand_chacha-0.2.2|rand_chacha|0.2.2||false|true", "rand_core-0.5.1|rand_core|0.5.1||false|true", "errno|errno|0.3.14||true|true", + "allocator_api2|allocator-api2|0.2.21||false|true", + "cbindgen|cbindgen|0.29.4||true|true", + "foldhash|foldhash|0.2.0||false|true", + "serde_spanned|serde_spanned|1.1.1||false|true", + "toml-0.9.12+spec-1.1.0|toml|0.9.12+spec-1.1.0||false|true", + "toml_datetime|toml_datetime|0.7.5+spec-1.1.0||false|true", + "toml_parser|toml_parser|1.1.3+spec-1.1.0||false|true", + "toml_writer|toml_writer|1.1.2+spec-1.1.0||false|true", + "winnow|winnow|1.0.4||false|true", ], ) @@ -1501,6 +1514,7 @@ rust_repo( version = "0.1.14", hashes = ["7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"], indirect = True, + platforms = [], ) # Added by please_rust sync @@ -1537,11 +1551,12 @@ rust_repo( # Added by please_rust sync rust_repo( - name = "log", + name = "log-0.4.33", crate = "log", version = "0.4.33", hashes = ["0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"], indirect = True, + platforms = [], ) rust_repo( @@ -1606,28 +1621,28 @@ rust_repo( ) rust_repo( - name = "heck-0.4.1", + name = "heck", crate = "heck", version = "0.4.1", hashes = ["95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"], ) rust_repo( - name = "itertools-0.10.5", + name = "itertools", crate = "itertools", version = "0.10.5", hashes = ["b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"], ) rust_repo( - name = "libsecp256k1_gen_ecmult-0.2.1", + name = "libsecp256k1_gen_ecmult", crate = "libsecp256k1-gen-ecmult", version = "0.2.1", hashes = ["ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3"], ) rust_repo( - name = "libsecp256k1_gen_genmult-0.2.1", + name = "libsecp256k1_gen_genmult", crate = "libsecp256k1-gen-genmult", version = "0.2.1", hashes = ["67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d"], @@ -1663,3 +1678,78 @@ rust_repo( version = "0.3.14", hashes = ["39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"], ) + +# Added by please_rust sync +rust_repo( + name = "allocator_api2", + crate = "allocator-api2", + version = "0.2.21", + hashes = ["683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"], + indirect = True, + platforms = [], +) + +rust_repo( + name = "cbindgen", + crate = "cbindgen", + version = "0.29.4", + hashes = ["2ecb53484c9c167ba674026b656d8a27d7657a58e6066aa902bfb1a4aa00ae20"], +) + +rust_repo( + name = "foldhash", + crate = "foldhash", + version = "0.2.0", + hashes = ["77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"], + indirect = True, + platforms = [], +) + +rust_repo( + name = "serde_spanned", + crate = "serde_spanned", + version = "1.1.1", + hashes = ["6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"], + indirect = True, +) + +rust_repo( + name = "toml-0.9.12+spec-1.1.0", + crate = "toml", + version = "0.9.12+spec-1.1.0", + hashes = ["cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"], + indirect = True, +) + +rust_repo( + name = "toml_datetime", + crate = "toml_datetime", + version = "0.7.5+spec-1.1.0", + hashes = ["92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"], + indirect = True, +) + +rust_repo( + name = "toml_parser", + crate = "toml_parser", + version = "1.1.3+spec-1.1.0", + hashes = ["1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"], + indirect = True, +) + +rust_repo( + name = "toml_writer", + crate = "toml_writer", + version = "1.1.2+spec-1.1.0", + hashes = ["7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"], + indirect = True, + platforms = [], +) + +rust_repo( + name = "winnow", + crate = "winnow", + version = "1.0.4", + hashes = ["23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"], + indirect = True, +) diff --git a/tools/please_rust/src/sync.rs b/tools/please_rust/src/sync.rs index 5ce7835..75d7116 100644 --- a/tools/please_rust/src/sync.rs +++ b/tools/please_rust/src/sync.rs @@ -883,8 +883,9 @@ fn import_cargo_lock(path: &Path, decls: &mut Vec) -> Result<()> { Ok(()) } -/// Newest declared version of a crate gets the plain normalized name; older -/// duplicates get `crate_norm-x.y.z`. Returns old->new subrepo renames. +/// A root declaration of a crate gets the plain normalized name, newest +/// first; everything else gets `crate_norm-x.y.z`. Returns old->new subrepo +/// renames. fn normalize_names(decls: &mut [Decl]) -> Result> { let mut by_crate: BTreeMap> = BTreeMap::new(); for (i, d) in decls.iter().enumerate() { @@ -900,7 +901,15 @@ fn normalize_names(decls: &mut [Decl]) -> Result> { .with_context(|| format!("Bad version {} for {}", decls[i].version, crate_name))?; versions.push((v, i)); } - versions.sort_by(|a, b| b.0.cmp(&a.0)); + // The bare name goes to a root before it goes to whatever is newest. + // A root is what somebody declared and what first-party rules name: + // //third_party/crates:toml. Handing the bare name to the newest + // version instead means a new major arriving as an indirect + // dependency of something unrelated takes the label, and every rule + // depending on it moves a major version without anyone saying so. + // Adding cbindgen, which wants toml 0.9, silently moved please_rust + // from the toml 0.8 it asks for. + versions.sort_by(|a, b| decls[b.1].root.cmp(&decls[a.1].root).then(b.0.cmp(&a.0))); for (rank, (_, i)) in versions.iter().enumerate() { let new_name = if rank == 0 { norm.clone() @@ -2062,6 +2071,45 @@ pub fn target_applies(target_cfg: &str, triple: &str) -> bool { mod tests { use super::*; + /// The bare label is what first-party rules name, so it has to keep + /// meaning the same crate. Adding cbindgen, which wants toml 0.9, handed + /// the bare toml label to that indirect entry and moved please_rust off + /// the toml 0.8 it asks for. The symptom was an unrelated test failing to + /// parse a lockfile. + #[test] + fn the_bare_name_goes_to_a_root_not_to_whatever_is_newest() { + let decl = |crate_name: &str, version: &str, root: bool| { + let mut d = parse( + "rust_repo(\n name = \"x\",\n crate = \"x\",\n version = \"1.0.0\",\n)\n", + ) + .remove(0); + d.crate_name = crate_name.to_string(); + d.version = version.to_string(); + d.root = root; + d.name = None; + d + }; + + // A root and a newer indirect: the root keeps the name it is + // depended on by. + let mut decls = vec![decl("toml", "0.8.23", true), decl("toml", "0.9.12", false)]; + normalize_names(&mut decls).unwrap(); + assert_eq!(decls[0].subrepo(), "toml"); + assert_eq!(decls[1].subrepo(), "toml-0.9.12"); + + // Two roots: the newest wins, as before. + let mut decls = vec![decl("serde", "1.0.0", true), decl("serde", "2.0.0", true)]; + normalize_names(&mut decls).unwrap(); + assert_eq!(decls[1].subrepo(), "serde"); + assert_eq!(decls[0].subrepo(), "serde-1.0.0"); + + // Nothing is a root: the newest wins, as before. + let mut decls = vec![decl("log", "0.4.1", false), decl("log", "0.4.9", false)]; + normalize_names(&mut decls).unwrap(); + assert_eq!(decls[1].subrepo(), "log"); + assert_eq!(decls[0].subrepo(), "log-0.4.1"); + } + fn parse(text: &str) -> Vec { let lines: Vec = text.lines().map(|s| s.to_string()).collect(); parse_build(&lines).unwrap()