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

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 cc_bindings_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:

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:

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.

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:

{{ #include ../../examples/cpp/carcinize/math_utils.h }}

When migrated, Carcinize generates math_utils_rs.rs:

{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}

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

{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}

NOTE: cc_bindings_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!

{{ #include ../../examples/cpp/carcinize/math_utils_rs.rs }}

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

BUILD file transformations {#build_transformations}

Before migration

Consider a C++ library target:

{{ #include ../../examples/cpp/carcinize/BUILD }}

After migration

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.

Generated Rust code {#generated_code}

For a C++ library like:

{{ #include ../../examples/cpp/carcinize/point.h }}

Carcinize generates point_rs.rs:

{{ #include ../../examples/cpp/carcinize/point_rs.rs }}

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.

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

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

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.