Skip to content

Commit 18013fc

Browse files
andrewle10copybara-github
authored andcommitted
Add carcinize CLI tool for automated C++ to Rust migration
PiperOrigin-RevId: 947706047
1 parent ebaef63 commit 18013fc

38 files changed

Lines changed: 615 additions & 178 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cargo/cc_bindings_from_rs/cpp_api_from_rust_lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ clap.workspace = true
3030
itertools.workspace = true
3131
crubit_feature = { path = "../../../cargo/common/crubit_feature"}
3232
flagset.workspace = true
33+
quote.workspace = true
3334
rustversion.workspace = true
3435
[dev-dependencies]
3536
run_compiler_test_support = { path = "../../../cargo/cc_bindings_from_rs/run_compiler_test_support", package = "cc_bindings_from_rs_run_compiler_test_support"}

cc_bindings_from_rs/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ rust_library(
3939
"@crate_index//:clap", # v4
4040
"@crate_index//:flagset", # v0_4
4141
"@crate_index//:itertools", # v0_13
42+
"@crate_index//:quote", # v1
4243
],
4344
)
4445

cc_bindings_from_rs/bazel_support/cc_bindings_from_rust_library_config_aspect_hint.bzl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,14 @@ def get_additional_cc_hdrs_and_srcs(aspect_hints):
7373
"""Returns any additional C++ headers and sources that should be compiled with the generated bindings.
7474
7575
Args:
76-
aspect_ctx: The ctx from an aspect_hint.
76+
aspect_hints: A list of aspect hints (Target list).
7777
7878
Returns:
7979
A list of `File` and its module paths as specified by the `extra_rs_srcs`.
8080
"""
8181
additional_cc_hdrs = []
8282
additional_cc_srcs = []
83+
8384
for hint in aspect_hints:
8485
if CcBindingsFromRustLibraryConfigInfo in hint:
8586
for target in hint[CcBindingsFromRustLibraryConfigInfo].extra_cc_hdrs:

cc_bindings_from_rs/bazel_support/cc_bindings_from_rust_rule.bzl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ def _generate_bindings(ctx, dep_bindings_infos, config, label, features, cli_fla
261261
crubit_args.add("--source-crate-name", self_crate_name)
262262
if self_rmeta != None:
263263
crubit_args.add("--extern={}={}".format(self_crate_name, self_rmeta.path))
264+
264265
crubit_args.add("--enable-rmeta-interface")
265266
is_golden_test = is_golden_test_override if is_golden_test_override != None else ctx.attr._is_golden_test[BuildSettingInfo].value
266267
if is_golden_test:
@@ -393,6 +394,8 @@ def _cc_bindings_from_rust_aspect_impl(target, ctx):
393394

394395
if CrateInfo not in target:
395396
return []
397+
if CcBindingsFromRustInfo in target:
398+
return []
396399
if str(target.label) in targets_to_remove:
397400
return []
398401

@@ -486,6 +489,11 @@ def _cc_bindings_from_rust_aspect_impl(target, ctx):
486489
aspect_hints = ctx.rule.attr.aspect_hints,
487490
rust_infos = dep_bindings_infos,
488491
)
492+
(extra_cc_hdrs, extra_cc_srcs) = get_additional_cc_hdrs_and_srcs(ctx.rule.attr.aspect_hints)
493+
for hdr in extra_cc_hdrs:
494+
if hdr.short_path.endswith("_extracted_cc.h"):
495+
cli_flags.append("--extra-rs-srcs-include=" + hdr.short_path)
496+
489497
bindings_info, features, config, output_depset = _generate_bindings(
490498
ctx,
491499
dep_bindings_infos = dep_bindings_infos,
@@ -504,8 +512,6 @@ def _cc_bindings_from_rust_aspect_impl(target, ctx):
504512

505513
dep_variant_info = _compile_rs_out_file(ctx, ctx.rule.attr, bindings_info.rust_file, target[CrateInfo].name, [target])
506514

507-
(extra_cc_hdrs, extra_cc_srcs) = get_additional_cc_hdrs_and_srcs(ctx.rule.attr.aspect_hints)
508-
509515
cc_info = _make_cc_info_for_h_out_file(
510516
ctx,
511517
bindings_info.h_file,

cc_bindings_from_rs/cmdline.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ pub struct Cmdline {
141141
value_name = "CRATE_NAME=RENAMED")]
142142
pub crate_rename: Vec<(String, String)>,
143143

144+
/// Extra headers to `#include` and re-export via IWYU in the generated C++ API header.
145+
///
146+
/// Re-exports user headers required by embedded C++ declarations (such as `inline_cpp!`).
147+
/// This ensures C++ callers using the generated C++ API header automatically receive
148+
/// the necessary includes for those types without encountering missing symbol errors.
149+
#[clap(long = "extra-rs-srcs-include", value_name = "FILE")]
150+
pub extra_rs_srcs_includes: Vec<String>,
151+
144152
/// The name of the crate to generate bindings for.
145153
///
146154
/// If provided, this value must correspond to the name of an extern crate provided to the

cc_bindings_from_rs/lib.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use rustc_middle::ty::TyCtxt;
1919
use rustc_session::config::OptionsTargetModifiers;
2020

2121
use arc_anyhow::{bail, Context, Result};
22-
use std::collections::{HashMap, HashSet};
22+
use std::collections::{BTreeSet, HashMap, HashSet};
2323
use std::path::{Path, PathBuf};
2424
use std::rc::Rc;
2525

@@ -121,11 +121,24 @@ fn run_with_tcx(cmdline: &Cmdline, tcx: TyCtxt) -> Result<()> {
121121
ErrorReport::new_rc_or_ignore(generate_error_report, SourceLanguage::Rust);
122122
let fatal_errors = Rc::new(FatalErrors::new());
123123

124-
let BindingsTokens { cc_api, cc_api_impl } = {
124+
let BindingsTokens { mut cc_api, cc_api_impl } = {
125125
let db = new_db(cmdline, tcx, errors, fatal_errors.clone());
126126
generate_bindings(&db)?
127127
};
128128

129+
if !cmdline.extra_rs_srcs_includes.is_empty() {
130+
let extra_includes: BTreeSet<CcInclude> = cmdline
131+
.extra_rs_srcs_includes
132+
.iter()
133+
.map(|include| CcInclude::exported_user_header(include.as_str().into()))
134+
.collect();
135+
let extra_includes_tokens = code_gen_utils::format_cc_includes(&extra_includes);
136+
cc_api = quote::quote! {
137+
#extra_includes_tokens
138+
#cc_api
139+
};
140+
}
141+
129142
let fatal_error_message = fatal_errors.take_string();
130143
if !fatal_error_message.is_empty() {
131144
return Err(arc_anyhow::Error::msg(fatal_error_message));

common/code_gen_utils.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,8 @@ pub enum CcInclude {
959959
/// format specifier for what comes after `#include` and path of the support
960960
/// library header.
961961
SupportLibHeader(Format<1>, Rc<str>),
962+
/// Represents a user header which should be re-exported by IWYU.
963+
ExportedUserHeader(Rc<str>),
962964
}
963965

964966
impl CcInclude {
@@ -1043,6 +1045,11 @@ impl CcInclude {
10431045
Self::UserHeader(path)
10441046
}
10451047

1048+
/// Creates a user include: `#include "some/path/to/header.h" // IWYU pragma: export`
1049+
pub fn exported_user_header(path: Rc<str>) -> Self {
1050+
Self::ExportedUserHeader(path)
1051+
}
1052+
10461053
/// Creates a `CcInclude` and detects whether it's a system header or a user
10471054
/// header based on the path.
10481055
///
@@ -1074,6 +1081,9 @@ impl ToTokens for CcInclude {
10741081
Self::UserHeader(path) => {
10751082
quote! { __HASH_TOKEN__ include #path __NEWLINE__ }.to_tokens(tokens)
10761083
}
1084+
Self::ExportedUserHeader(path) => {
1085+
quote! { __HASH_TOKEN__ include #path __COMMENT__ "IWYU pragma: export" __NEWLINE__ }.to_tokens(tokens)
1086+
}
10771087
Self::SupportLibHeader(format, path) => {
10781088
let full_path: TokenStream = format
10791089
.format(&[&*path])

rs_bindings_from_cc/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,13 +421,20 @@ cc_library(
421421
],
422422
)
423423

424+
rust_library(
425+
name = "bazel_label",
426+
srcs = ["bazel_label.rs"],
427+
visibility = ["//visibility:public"],
428+
)
429+
424430
rust_library(
425431
name = "ir",
426432
srcs = [
427433
"ir.rs",
428434
],
429435
visibility = ["//:__subpackages__"],
430436
deps = [
437+
":bazel_label",
431438
"//common:arc_anyhow",
432439
"//common:code_gen_utils",
433440
"//common:crubit_feature",

rs_bindings_from_cc/bazel_label.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Part of the Crubit project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
use std::cmp::Ordering;
6+
use std::fmt::{self, Display};
7+
use std::hash::{Hash, Hasher};
8+
use std::rc::Rc;
9+
10+
/// A Bazel label, e.g. `//foo:bar`.
11+
#[derive(Debug, Eq, Clone)]
12+
pub struct BazelLabel(pub Rc<str>);
13+
14+
impl BazelLabel {
15+
pub fn as_str(&self) -> &str {
16+
&self.0
17+
}
18+
19+
/// Returns the target name. E.g. `bar` for `//foo:bar`.
20+
pub fn target_name(&self) -> &str {
21+
if let Some((_package, target_name)) = self.0.split_once(':') {
22+
return target_name;
23+
}
24+
if let Some((_, last_package_component)) = self.0.rsplit_once('/') {
25+
return last_package_component;
26+
}
27+
&self.0
28+
}
29+
30+
pub fn package_name(&self) -> &str {
31+
self.0.rsplit_once(':').unwrap_or((&self.0, "")).0
32+
}
33+
34+
fn last_package_component(&self) -> &str {
35+
self.package_name().rsplit_once('/').unwrap_or(("", "")).1
36+
}
37+
38+
// TODO(b/216587072): Remove this hacky escaping and use the import! macro once
39+
// available.
40+
// For now, use the simple escaping scheme of mapping all invalid characters
41+
// to underscore, instead of the one similar to `convert_to_cc_identifier`, so
42+
// that the escaped target name doesn't become longer (rustc currently produces
43+
// .o artifacts that repeat the target name twice, which can easily cause
44+
// the path length of artifacts to exceed the limit of the file system.)
45+
pub fn target_name_escaped(&self) -> String {
46+
let mut target_name = self.target_name().to_owned();
47+
if target_name == "core" {
48+
target_name = "core_".to_owned() + self.last_package_component();
49+
} else if target_name.starts_with(char::is_numeric) {
50+
target_name.insert(0, 'n');
51+
}
52+
target_name.replace(|c: char| !c.is_ascii_alphanumeric(), "_")
53+
}
54+
55+
// Returns the bazel label as a valid C++ identifier, with a leading underscore.
56+
// Non-alphanumeric characters are escaped as `_xx`, where `xx` is the the byte
57+
// as hexadecimal.
58+
//
59+
// For instance, `//foo` becomes `__2f_2ffoo`.
60+
pub fn convert_to_cc_identifier(&self) -> String {
61+
use std::fmt::Write;
62+
let mut result = "_".to_string();
63+
result.reserve_exact(self.0.len().checked_mul(2).unwrap_or(self.0.len()));
64+
65+
// This is yet another escaping scheme... :-/ Compare this with
66+
// https://github.com/bazelbuild/rules_rust/blob/1f2e6231de29d8fad8d21486f0d16403632700bf/rust/private/utils.bzl#L459-L586
67+
for b in self.0.bytes() {
68+
if (b as char).is_ascii_alphanumeric() {
69+
result.push(b as char);
70+
} else {
71+
write!(result, "_{b:02x}").unwrap();
72+
}
73+
}
74+
result.shrink_to_fit();
75+
76+
#[cfg(debug_assertions)]
77+
for c in result.chars() {
78+
debug_assert!(
79+
c.is_ascii_alphanumeric() || c == '_',
80+
"invalid result identifier: {result:?}"
81+
);
82+
}
83+
84+
result
85+
}
86+
87+
fn components(&self) -> (&str, &str) {
88+
(self.target_name(), self.package_name())
89+
}
90+
}
91+
92+
impl PartialEq for BazelLabel {
93+
fn eq(&self, other: &Self) -> bool {
94+
self.components() == other.components()
95+
}
96+
}
97+
98+
impl PartialOrd for BazelLabel {
99+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
100+
Some(self.cmp(other))
101+
}
102+
}
103+
104+
impl Ord for BazelLabel {
105+
fn cmp(&self, other: &Self) -> Ordering {
106+
self.components().cmp(&other.components())
107+
}
108+
}
109+
110+
impl Hash for BazelLabel {
111+
fn hash<H: Hasher>(&self, state: &mut H) {
112+
self.components().hash(state);
113+
}
114+
}
115+
116+
impl<T: Into<String>> From<T> for BazelLabel {
117+
fn from(label: T) -> Self {
118+
Self(label.into().into())
119+
}
120+
}
121+
122+
impl Display for BazelLabel {
123+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124+
// If this isn't actually a known bazel target, stringify for humans as the filename.
125+
if let Some(s) = self.0.strip_prefix("//_unknown_target:") {
126+
write!(f, "{}", s)
127+
} else {
128+
write!(f, "{}", &*self.0)
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)