Carbon's Pointer and Reference Model: An Architecture Note on C++ Interoperability Boundaries
Carbon's experimental pointer and reference model aims to make C++ interoperability memory-safe without wrapper overhead. This architecture note examines the trust boundary, operational checks, and failure modes.
13 Jan 2026, 02:53 UTC

The migration problem Carbon is trying to solve
Moving a large C++ system to a memory-safe language usually forces a trade-off. Rust offers strong guarantees but requires foreign function interface (FFI) wrappers and unsafe blocks to talk to C++, which adds friction. Managed runtimes remove manual memory control entirely. Carbon's experimental design proposes a different path: keep C++ and Carbon in the same binary and introduce a pointer and reference model that makes safety explicit at the boundary rather than hiding it behind a runtime.
The useful takeaway is that Carbon's safety story is not automatic. It is a set of compile-time rules that depend on how carefully you define the trust boundary between Carbon and C++ code. If that boundary is loose, the guarantees weaken.
Requirements for a C++-compatible safety model
According to the project's design goals, the memory model must satisfy four constraints:
- Bidirectional interoperability without significant wrapper overhead. Existing C++ functions should be callable from Carbon, and Carbon functions callable from C++, without generating thick adapter layers.
- Improved memory safety over raw C++. The language should catch invalid memory access at compile time where possible.
- Gradual migration. Developers must be able to convert one file or module at a time, mixing both languages in a single binary.
- Flexible memory management. Both manual patterns and automated or scoped patterns should be expressible.
The smallest suitable design: references vs. pointers
Carbon's core answer is to split what C++ calls a pointer into two distinct concepts. A reference is intended to be always valid and non-null; dereferencing it is safe by construction. A pointer is explicitly nullable and may point to invalid memory, requiring the programmer to check before use.
This split creates a safe default for the common case and an explicit escape hatch for low-level work. On top of this, Carbon uses borrowing—temporary access to an object without taking ownership. The borrowing rules are distinct from Rust's; they are designed to feel closer to C++ lifetime semantics while still letting the compiler track temporary access.
The result is a deliberately small design: references carry safety invariants, pointers carry risk, and borrowing mediates temporary access. Nothing in the model requires a garbage collector or a Rust-style borrow checker.
Trust and data boundaries: where safety ends
The critical architecture decision is not inside Carbon code—it is at the C++ interoperability boundary. Carbon can enforce its reference and pointer rules internally, but it cannot prove that an arbitrary C++ function returned a valid pointer. The boundary is therefore a trust domain edge.
When data crosses from C++ into Carbon, it enters a zone where Carbon's compiler can apply local checks but cannot audit the C++ caller's behavior. The practical rule is to treat every raw pointer arriving from C++ as untrusted until validated. Ownership and lifetime expectations must be declared explicitly at the API surface; implicit assumptions will break.
Because the design goal is minimal wrapper overhead, the boundary is likely to be thin. That efficiency is also the risk: a thin boundary means fewer automatic guardrails.
Operational checks before relying on the model
Carbon is experimental and not yet a stable production language. Before depending on its pointer semantics, verify the current state directly:
- Review the official Carbon Language GitHub repository for the latest design documents. The specification is subject to significant change.
- Consult the C++ interoperability specification drafts to understand how ownership and lifetime annotations are expected to work at the boundary.
- Compile sample code with the experimental compiler to observe how pointer and reference types behave in practice. Do not assume syntax or semantics from older drafts.
- Check that ownership transfer is explicit in any interop API you design. If the compiler does not force you to state who owns a piece of memory, treat that as a design gap to close manually.
Failure modes and conditions that would change the design
Several failure modes are plausible given the experimental status:
| Failure mode | Why it happens | Mitigation |
|---|---|---|
| Boundary erosion | C++ returns a pointer to stack or freed memory; Carbon's reference rules are bypassed if the boundary type is loose. | Validate all incoming C++ pointers; prefer safe wrapper types at the boundary. |
| Specification drift | The language is pre-1.0; syntax and ownership rules may change. | Pin to a specific compiler version; isolate Carbon modules behind stable interfaces. |
| Ownership ambiguity | Bidirectional calls make it unclear which language is responsible for deallocation. | Document and enforce ownership at every crossing; avoid implicit transfers. |
| Annotation overhead | If interop requires heavy lifetime annotations, the gradual migration path slows. | Measure annotation cost on a representative module before committing. |
The design would need to change if interoperability proves to require thick wrappers after all, if the safety guarantees leak too easily across the boundary, or if the ownership model becomes too complex for incremental adoption. A stable specification release and mature toolchain would also shift these calculations.
A conceptual comparison
The following table contrasts Carbon's intended model with C++ and Rust on the dimensions that matter for migration.
| Aspect | C++ | Rust | Carbon (intended design) |
|---|---|---|---|
| Null pointers | Allowed, unchecked | Option<T> wrapper | Pointers explicitly nullable; references non-null |
| Interop cost | N/A | FFI wrappers and unsafe blocks | Bidirectional, minimal wrappers (design goal) |
| Ownership model | Manual / RAII | Strict borrow checker | Borrowing distinct from Rust, closer to C++ lifetimes |
| Migration path | N/A | Rewrite or bindings | Gradual, same binary |
None of these rows should be treated as verified production behavior. They describe the direction of the experimental design and must be confirmed against current compiler output and specification drafts.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.