Carcinize

Carcinize (carcinize) is a migration tool that automatically converts existing C++ libraries to Rust libraries containing embedded C++ code via inline_cpp!.

Overview

Carcinize makes it possible to migrate C++ libraries to Rust incrementally. Carcinize generates a Rust crate where:

  1. C++ Structs are mapped to layout-compatible Rust structs.
  2. Rust Functions are generated to wrap the original C++ functions using inline_cpp! blocks.
  3. Downstream C++ Callers continue using the library through generated cpp_api_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.

How to use Carcinize

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).

Target mode (Bazel) {#target_mode}

To migrate a cc_library in your workspace, run:

bazel run //google_internal/carcinize:carcinize -- //path/to/pkg:my_target

Carcinize will:

  1. Generate the Rust source file //path/to/pkg/my_target_rs.rs.
  2. Update //path/to/pkg/BUILD to replace the cc_library with a rust_library_with_embedded_cpp and re-export the original target name via cpp_api_from_rust.

After the command completes, verify the generated target with your unit tests:

bazel test //path/to/pkg:...

Standalone mode {#standalone_mode}

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:

bazel run //google_internal/carcinize:carcinize -- \
  --headers path/to/header.h \
  --srcs path/to/source.cc \
  --out path/to/output_rs.rs

Handling partially supported libraries (Incomplete Migration) {#incomplete_migration}

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:

bazel run //google_internal/carcinize: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, the Crubit bindings build fails with an error for each unsupported declaration (for example, Function templates are not yet supported), and Carcinize halts.

How Incomplete Migration works {#how_incomplete_works}

By default, Carcinize partitions declarations:

  1. Supported structs are generated as layout-compatible Rust struct definitions.
  2. Supported functions are generated as Rust functions whose bodies wrap the C++ implementation using inline_cpp! blocks.
  3. Unsupported declarations (such as class or function templates) are emitted inside companion global_cpp! blocks in the generated Rust file.

Incomplete Migration example {#incomplete_example}

Consider a C++ header containing both a standard function and a C++ template:

cs/file:examples/cpp/carcinize/math_utils.h content:^namespace\s+math\s*\{[\s\S]*?^\}\s*//\s*namespace\s+math

When migrated, Carcinize generates math_utils_rs.rs:

cs/file:examples/cpp/carcinize/math_utils_rs.rs content:^\s*global_cpp!\s*\{[\s\S]*?^\}\s*pub\s+mod\s+math\s*\{[\s\S]*?^\}

Caller access {#incomplete_downstream}

rust_library_with_embedded_cpp extracts the companion C++ code from global_cpp! and compiles it into a companion C++ header. cpp_api_from_rust re-exports both the Rust bindings and the companion header back to C++ callers.

  • C++ callers retain access to all declarations, including templates (Clamp<T>).
  • Rust code in the crate can invoke templates from global_cpp! with concrete types inside inline_cpp! blocks (for example, math::Clamp(val, min, max)).
  • Rust callers outside the crate use the generated Rust structs and functions (Vector2, DotProduct).

Migrating fallback C++ declarations to pure Rust {#resolving_fallbacks}

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:

Strategy 1: Rewrite in Rust (for Rust callers)

cs/file:examples/cpp/carcinize/math_utils_rs.rs function:clamp

NOTE: cpp_api_from_rust does not currently generate C++ function templates from Rust generic functions. If existing C++ callers still require the C++ template, keep the declaration in the global_cpp! block (or provide concrete C++ wrappers as shown below) until downstream C++ callers are migrated.

Strategy 2: Wrap specific instantiations with inline_cpp!

cs/file:examples/cpp/carcinize/math_utils_rs.rs function:clamp_i32

Clean up fallback blocks

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.

Governance guards and incremental migration

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 rename the fallback global_cpp! block to DO_NOT_SUBMIT_CPP_DECL! by hand. C++ extraction treats it the same as global_cpp!:

DO_NOT_SUBMIT_CPP_DECL! {
    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.

BUILD file transformations {#build_transformations}

Before migration

Consider a C++ library target:

cs/file:examples/cpp/carcinize/BUILD symbol:point_before

After migration

Carcinize updates the BUILD file to define a rust_library_with_embedded_cpp target for the migrated Rust library:

cs/file:examples/cpp/carcinize/BUILD symbol:point_rs

And re-exports the library back to C++ callers under the original target name using cpp_api_from_rust:

cs/file:examples/cpp/carcinize/BUILD symbol:\bpoint\b
  • 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.
  • cpp_api_from_rust (:point): Exposes the Rust library back to C++ callers under the original target name, so downstream dependencies require no changes.

Generated Rust code {#generated_code}

For a C++ library like:

cs/file:examples/cpp/carcinize/point.h content:^namespace\s+geometry\s*\{[\s\S]*?^\}\s*//\s*namespace\s+geometry

Carcinize generates point_rs.rs:

cs/file:examples/cpp/carcinize/point_rs.rs content:^\s*global_cpp!\s*\{[\s\S]*?^\}\s*pub\s+mod\s+geometry\s*\{[\s\S]*?^\}

Refactoring workflow

Once Carcinize generates the initial Rust scaffolding, you can incrementally refactor the crate into idiomatic Rust:

  1. Verify Baseline: Run bazel test //path/to/pkg:... to confirm the generated target compiles and passes existing unit tests.
  2. Rewrite Functions in Rust: Replace inline_cpp! blocks one function at a time with pure Rust implementations.
  3. 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).
  4. 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.

For example, when rewriting functions in step 2, convert 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

Common errors

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 collision: <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.

Unsupported C++ declarations {#error_unsupported}

When --require_complete_migration is enabled and any unbindable declarations are encountered, the Crubit bindings build fails with an error for each unsupported declaration (for example, Function templates are not yet supported), and Carcinize halts.

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.

Missing C++ dependencies {#error_missing_headers}

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).

Unmatched braces in embedded C++ {#error_unmatched_braces}

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.