Carcinize (carcinize) is a migration tool that automatically converts existing C++ libraries to Rust libraries containing embedded C++ code via inline_cpp!.
Carcinize makes it possible to migrate C++ libraries to Rust incrementally. Carcinize generates a Rust crate where:
inline_cpp! blocks.cc_bindings_from_rust targets.The generated Rust target will result in the same API and use the same implementation as the original C++ target, but will be entirely within a Rust source file. You can then refactor functions one at a time from embedded C++ into idiomatic Rust while verifying behavior with tests at every step.
Carcinize can be run directly on a cc_library target in your workspace (the standard workflow for Bazel users) or in standalone mode on source files (for non-Bazel environments, automated developer tooling, and AI agents).
To migrate a cc_library in your workspace, run:
bazel run //google_internal/carcinize:carcinize -- //path/to/pkg:my_target
Carcinize will:
//path/to/pkg/my_target_rs.rs.//path/to/pkg/BUILD to replace the cc_library with a rust_library_with_embedded_cpp and re-export the original target name via cc_bindings_from_rust.After the command completes, verify the generated target with your unit tests:
bazel test //path/to/pkg:...
For developers in non-Bazel environments (such as Cargo or CMake projects), as well as automated developer tooling, AI agents, and custom scripts, Carcinize provides a standalone CLI mode that operates directly on C++ headers and source files without evaluating Bazel queries or mutating BUILD files:
carcinize \ --headers path/to/header.h \ --srcs path/to/source.cc \ --out path/to/output_rs.rs
Some C++ libraries contain features that Crubit does not yet bind automatically, such as C++ function templates or unbindable types.
By default, Carcinize allows incomplete migrations by partitioning unsupported declarations into companion global_cpp! blocks in the generated Rust crate so the migrated crate compiles out-of-the-box.
If you instead want to strictly require complete Crubit binding generation and fail if any unsupported C++ declarations are present, pass --require_complete_migration (or --require-complete-migration):
bazel run //google_internal/carcinize:carcinize -- \ --require_complete_migration //path/to/pkg:my_target
In standalone mode:
carcinize \ --require_complete_migration \ --headers path/to/header.h \ --srcs path/to/source.cc \ --out path/to/output_rs.rs
When --require_complete_migration is enabled and unsupported C++ declarations are present, Carcinize halts with an error:
Error: Target //math:math_utils contains unsupported C++ declarations: - class template `math::UnsupportedTemplate` (Class templates are not yet supported) - function template `math::Clamp` (Function templates are not yet supported) Migration aborted because --require_complete_migration was specified.
By default, Carcinize partitions declarations:
struct definitions.inline_cpp! blocks.global_cpp! blocks in the generated Rust file.Consider a C++ header containing both a standard function and a C++ template:
{{ #include ../../examples/cpp/carcinize/math_utils.h }}
When migrated, Carcinize generates math_utils_rs.rs:
{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}
rust_library_with_embedded_cpp extracts the companion C++ code from global_cpp! and compiles it into a companion C++ header. cc_bindings_from_rust re-exports both the Rust bindings and the companion header back to C++ callers.
Clamp<T>).global_cpp! with concrete types inside inline_cpp! blocks (for example, math::Clamp(val, min, max)).Vector2, DotProduct).The generated target compiles immediately without manual intervention.
Migrating fallback declarations to pure Rust is optional. If you choose to remove C++ dependencies entirely, you can replace the fallback global_cpp! declarations with Rust implementations over time using one of these strategies:
{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}
NOTE:
cc_bindings_from_rustdoes not currently generate C++ function templates from Rust generic functions. If existing C++ callers still require the C++ template, keep the declaration in theglobal_cpp!block (or provide concrete C++ wrappers as shown below) until downstream C++ callers are migrated.
inline_cpp!{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}
Once all declarations in global_cpp! have been ported to Rust or are no longer needed, delete the global_cpp! block from the generated file and remove the corresponding C++ dependencies from deps_of_cc_library in your BUILD file.
When performing an incremental migration across multiple CLs, fallback global_cpp! blocks can be safely checked in and maintained while downstream callers or unsupported features are migrated over time.
However, if your team wants to enforce complete migration in a single change (or prevent unmigrated C++ declarations from being accidentally committed during local experimentation), you can pass --macro_name DO_NOT_SUBMIT_CPP_DECL!:
bazel run //google_internal/carcinize:carcinize -- \ --macro_name DO_NOT_SUBMIT_CPP_DECL! \ //path/to/pkg:my_target
Carcinize generates the fallback block wrapped in DO_NOT_SUBMIT_CPP_DECL!:
// Generated with --macro_name DO_NOT_SUBMIT_CPP_DECL! DO_NOT_SUBMIT_CPP_DECL! { #include "math/math_utils.h" namespace math { template <typename T> T Clamp(T val, T min, T max) { return val < min ? min : (val > max ? max : val); } } // namespace math }
Piper presubmit checks will block submitting changes containing DO_NOT_SUBMIT blocks while still allowing local compilation and testing until all fallbacks are resolved. The build rules also emit compile-time warnings if declarations inside fallback blocks become natively supported by Crubit.
Consider a C++ library target:
{{ #include ../../examples/cpp/carcinize/BUILD }}
Carcinize updates the BUILD file to define a rust_library_with_embedded_cpp target for the migrated Rust library:
{{ #include ../../examples/cpp/carcinize/BUILD }}
And re-exports the library back to C++ callers under the original target name using cc_bindings_from_rust:
{{ #include ../../examples/cpp/carcinize/BUILD }}
rust_library_with_embedded_cpp (:point_rs): Compiles the generated Rust crate and formats/extracts embedded C++ blocks.deps_of_cc_library: Lists C++ dependencies required by C++ headers used in inline_cpp! and global_cpp! blocks.cc_bindings_from_rust (:point): Exposes the Rust library back to C++ callers under the original target name, so downstream dependencies require no changes.For a C++ library like:
{{ #include ../../examples/cpp/carcinize/point.h }}
Carcinize generates point_rs.rs:
{{ #include ../../examples/cpp/carcinize/point_rs.rs }}
Once Carcinize generates the initial Rust scaffolding, you can incrementally refactor the crate into idiomatic Rust:
Verify Baseline: Run bazel test //path/to/pkg:... to confirm the generated target compiles and passes existing unit tests.
Rewrite Functions in Rust: Replace inline_cpp! blocks one function at a time with pure Rust implementations.
For example, converting embedded C++:
cs/file:examples/cpp/carcinize/point_rs.rs function:GetX
Into pure, idiomatic Rust:
cs/file:examples/cpp/carcinize/point_refactored.rs function:GetX
Idiomatic Rust Types and Traits: Replace FFI types (such as ::ffi_11::c_int) with standard Rust types (such as i32), and derive standard traits (Debug, PartialEq, Default).
Remove C++ Dependencies: Once all embedded C++ is replaced with pure Rust, remove the global_cpp! blocks and deps_of_cc_library from your BUILD file.
Carcinize and inline_cpp! use Crubit under the hood, so any Crubit error can occur during migration. See crubit.rs/errors and Inline C++ in Crubit for related diagnostic guidance.
<target>_rs already exists {#error_collision}If a target named <target>_rs already exists in the BUILD file, Carcinize aborts to avoid overwriting existing code:
Error: Target //geometry:point_rs already exists. Aborting to prevent collisions.
Rename or remove the existing target before running Carcinize.
When --require_complete_migration is enabled, Carcinize halts with an error if any unbindable declarations are encountered:
Error: Target //math:math_utils contains unsupported C++ declarations: - class template `math::UnsupportedTemplate` (Class templates are not yet supported) - function template `math::Clamp` (Function templates are not yet supported) Migration aborted because --require_complete_migration was specified.
To proceed with migrating partially supported libraries without halting, omit --require_complete_migration to allow Carcinize to route those declarations into companion global_cpp! fallback blocks automatically.
If Clang fails to find #include headers during compilation:
error: 'third_party/absl/strings/str_cat.h' file not found
#include "third_party/absl/strings/str_cat.h"
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Ensure the target exporting those headers is listed in deps_of_cc_library in your rust_library_with_embedded_cpp rule (or in deps of the original cc_library).
If C++ code inside inline_cpp! or global_cpp! has unbalanced braces (for example, in macros or raw strings), the Rust compiler reports an error when parsing token trees:
error: unexpected closing delimiter: `}` --> math_utils_rs.rs:25:5 | 25| } | ^ unexpected closing delimiter
Ensure all opening and closing braces within the macro block are properly balanced.