bi-06 — Chromium C++ for Frontend Engineers

Phase 2, just-in-time · Spec area §4. Prerequisites: bi-01. Use alongside bi-03–bi-09.

Read this module in fragments, on demand. It is a reference, not a course. The correct way to use it is: hit an idiom you cannot read → look it up here → find a real example in the tree → carry on. Front-loading a C++ course before reading Blink is the single most common way people spend three weeks and learn nothing about browsers.

Cross-track hook: none.


0. Orientation

The 60-second version

You are not learning C++. You are learning the subset of C++ that makes Chromium readable, which is a much smaller and much more tractable thing.

The core of it is one question, asked of every pointer you meet:

Who owns this, and how long does it live?

Chromium answers that in the type, which is why the pointer vocabulary is 80% of reading comprehension:

TypeMeaning
std::unique_ptr<T>sole ownership; moved, never copied
scoped_refptr<T>shared ownership, refcounted
raw_ptr<T>non-owning member, hardened against use-after-free
T*non-owning, no guarantees — locals and parameters
T&non-owning, non-null

And in Blink's GC heap the vocabulary is different — Member, Persistent, WeakMember. Mixing up the two vocabularies is the most common newcomer error. Rule of thumb: inside blink:: classes that are GarbageCollected, use Oilpan handles; everywhere else, the table above.

The second reading skill

When you see a callback, ask what keeps the receiver alive. The answer is one of exactly four things: ownership, a scoped_refptr, a WeakPtr (so it may silently not run), or base::Unretained(this) — which is an explicit promise that the object outlives the callback.

Treat every Unretained as a lifetime claim to verify. It is where use-after-free lives.

Why the code looks the way it does

Much of Chromium's apparent verbosity is the memory-safety programme made visible: raw_ptr converting a class of use-after-free into crashes, CHECK vs DCHECK encoding what must hold in release, SEQUENCE_CHECKER as executable documentation of threading assumptions. Reading these as noise loses the information; reading them as assertions about invariants is the skill.

What you should be able to do after this module

CapabilityEvidence you have it
Read ownership from typesState a pointer's lifetime contract without reading the implementation
Audit a callbackName what keeps the receiver alive, and flag every Unretained
Read threading intentUse SEQUENCE_CHECKER and task runners as the concurrency documentation
Read a class coldApply a fixed checklist rather than reading top to bottom

Why a Principal Engineer needs this

You need to read Chromium C++ fluently and write a small, idiomatic amount of it. Those are different bars, and conflating them is why people over-prepare.

  • Reading requires: ownership idioms, callbacks, threading annotations, Oilpan handles, and the ability to ignore templates you do not need.
  • Writing (a §24 rung-2/3 contribution) additionally requires: matching local style, correct handle types, and not introducing lifetime bugs. Reviewers catch the rest.

You do not need: template metaprogramming, the standard-library algorithm catalogue, move-semantics edge cases, or exception handling — Chromium builds without exceptions, which removes a large chunk of normal C++ complexity from consideration.


The reading subset

Ownership: the four pointers

This is 80 % of reading comprehension. Every pointer in Chromium answers "who owns this and how long does it live."

TypeMeaningWhere
std::unique_ptr<T>sole ownership; moved, never copiedeverywhere outside Blink's GC heap
scoped_refptr<T>shared ownership, refcounted (RefCounted<T>)//base, //cc, task runners
raw_ptr<T>non-owning pointer, hardened against use-after-freemember fields in non-GC code
T*non-owning, no guaranteeslocals, parameters
T&non-owning, non-nullparameters that must exist

raw_ptr<T> surprises people coming from older C++ or older Chromium: raw pointer members are progressively being replaced by it because it converts a large class of use-after-free vulnerabilities into crashes. When you see it, read it as "non-owning member, and someone thought about lifetime here."

In Blink's GC heap the vocabulary is different — that is bi-04's table (Member, Persistent, WeakMember, …). Mixing up the two vocabularies is the most common newcomer error. Rule of thumb: inside blink:: classes that are GarbageCollected, use Oilpan handles; everywhere else, the table above.

RAII and Scoped*

A ScopedFoo does something in its constructor and undoes it in its destructor. When reading a function, Scoped* locals are the "and afterwards, this is restored" markers — they often encode the invariant more clearly than the surrounding code. Blink's parser, style engine and compositor all use them for state that must not leak across a scope.

Callbacks

Chromium's callbacks are base::OnceCallback / base::RepeatingCallback, created with base::BindOnce / base::BindRepeating. Documented at length in docs/callback.md.

What you must be able to read:

  • BindOnce(&Class::Method, receiver, args...) — the first bound argument is the receiver.
  • OnceCallback runs once and must be moved, not copied. std::move(callback) at a call site is why.
  • base::Unretained(this) is an explicit assertion: "I promise this outlives the callback." Treat every Unretained as a lifetime claim to verify — it is where use-after-free lives.
  • WeakPtr<T> + WeakPtrFactory<T> — the callback silently does nothing if the object died. The safe default in UI code.

Reading skill: when you see a callback, ask what keeps the receiver alive. The answer is one of: ownership, scoped_refptr, WeakPtr (may not run), or Unretained (a promise).

Threading annotations

SEQUENCE_CHECKER(sequence_checker_);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
scoped_refptr<base::SequencedTaskRunner> task_runner_;
task_runner_->PostTask(FROM_HERE, base::BindOnce(&Foo::Bar, ...));

SEQUENCE_CHECKER is executable documentation of "this object is only touched from one sequence" (bi-02). Read it as a hard constraint; violating it is exactly the bug class Blink's message-passing rule exists to avoid.

Strings

Blink uses WTF::String, AtomicString, StringView; Chromium proper uses std::string, std::u16string, std::string_view. AtomicString is interned — pointer comparison is identity comparison, which is why tag and attribute names are atoms. Conversions across the boundary allocate, and that is a real cost in the bindings layer (bi-05).

Containers and small types

  • WTF::Vector, WTF::HashMap, WTF::HashSet in Blink; base::flat_map, base::span, std::optional elsewhere.
  • base::span<T> — pointer+length view. Chromium is actively migrating raw pointer/length pairs to spans for memory safety; expect it in new code, not old.
  • std::optional<T> — "may be absent," instead of sentinel values.

Enums and the NOTREACHED() idiom

enum class everywhere. A switch over an enum ending in NOTREACHED() is a contract that the enum is exhaustive. Add an enumerator and the compiler finds the switches; NOTREACHED() catches the ones it cannot.

Generated code shapes

You will constantly read code whose definition is not in the tree (bi-01 Technique 4). Recognise the shapes:

  • mojom::FooPtr, mojo::Remote<mojom::Foo>, mojo::Receiver<mojom::Foo> — IPC endpoints.
  • V8Foo::… — generated bindings.
  • CSSPropertyID::kFoo, html_names::kDivTag — generated from .json5.

What to ignore

Template metaprogramming, constexpr machinery, SFINAE, allocator plumbing, and most of //base's internals. If a header looks like a type-level puzzle, it is almost certainly not what your question is about. Skipping confidently is a skill; bi-01's deferred-list discipline is how you do it without losing the thread.


The writing subset

For a first contribution you additionally need:

  1. Match the file you are editing. Style is enforced by git cl format — run it and stop thinking about formatting.
  2. Correct handle type. In Blink GC classes, Member<T> for owned graph edges. Getting this wrong is the most likely substantive review comment.
  3. DCHECK your assumptions. Adding a DCHECK stating your new invariant is usually welcomed, and it is how you document intent in a codebase that discourages comments restating code.
  4. Tests in the right style. Blink foo_test.cc, Chromium foo_unittest.cc. Copy the nearest existing test's structure exactly.
  5. No exceptions, no RTTI. If your instinct reaches for either, the design is off.

Deep dive: the style documents that actually govern

styleguide/c++/ is where the rules live. Sizes are informative:

DocumentSizeWhat it governs
c++-features.md~78 KBwhich C++ language/library features are allowed, banned, or under discussion
c++.md~20 KBthe general Chromium C++ style
c++-dos-and-donts.md~18 KBaccumulated guidance and anti-patterns
blink-c++.md~8 KBBlink-specific rules on top
const.md, checks.mdconst correctness; CHECK/DCHECK policy

Read c++-features.md before using any modern C++ feature. Chromium gates language features explicitly — a feature being in the standard does not mean it is permitted in the tree. Seventy-eight kilobytes of "allowed / banned / under discussion" is a governance system, not a style file, and proposing banned constructs in a CL is a fast way to burn reviewer patience.

blink-c++.md is short and its section headings are the whole message:

  • Prefer WTF types over STL and base types
  • Do not use new and delete
  • Don't mix Create() factory methods and public constructors in one class
  • Naming
  • Prefer enums or StrongAliases to bare bools for function parameters

That last one is worth adopting in your own work regardless of language. DoThing(true, false) is unreadable at the call site; DoThing(kAnimate, kDontNotify) is self-documenting. Chromium enforces at the type level what most style guides only suggest.

"Do not use new and delete" is the visible consequence of the ownership vocabulary: everything is MakeGarbageCollected<T>, std::make_unique<T>, or base::MakeRefCounted<T>. If you find yourself reaching for raw new, you have not decided who owns the object.


Deep dive: CHECK vs DCHECK, as a design decision

The distinction is not "expensive vs cheap." It is a statement about what kind of failure this is.

MacroRetained in release?Means
DCHECK(x)no"this should be true; if not, we have a bug"
CHECK(x)yes"if this is false, continuing is unsafe — crash instead"
NOTREACHED()yes"this state is impossible by construction"
DUMP_WILL_BE_CHECKstageda DCHECK being promoted to CHECK, with data collection first

The rule of thumb Chromium applies: if the invariant failing would be a security or memory-safety problem, it is a CHECK. Crashing is preferable to continuing with a violated invariant an attacker might exploit. If it would merely be a wrong pixel, it is a DCHECK.

Two things follow for you as a reader:

  1. A CHECK is a load-bearing invariant. When you find one, you have found something the authors decided was worth an outage to protect. Read it carefully before changing nearby code.
  2. DUMP_WILL_BE_CHECK is a migration in progress — someone wants this to be a CHECK but is collecting field data first to make sure it will not crash real users. It marks an invariant believed true but not yet trusted, which is genuinely useful context.

This is a pattern worth stealing: stage your assertions. Collect data that an invariant holds before you make violating it fatal.


Deep dive: the memory-safety programme, and why the code looks like it does

A great deal of modern Chromium C++ is shaped by an ongoing memory-safety effort. Recognising the pieces stops them looking like arbitrary style.

MechanismWhat it does
raw_ptr<T> (MiraclePtr)hardened non-owning member pointers; turns some UAF into a crash
base::span<T>replaces pointer+length pairs; bounds are carried with the data
PartitionAllocthe allocator, with partitioning that makes some exploitation harder
Rust interopnew, isolated, untrusted-input parsers written in a safe language
The Rule of Two (bi-02)the architectural constraint that drives sandboxing decisions
Clang pluginsmechanical enforcement of Blink/Chromium-specific rules

The span migration is the one you will notice most while reading: new code takes base::span<const uint8_t> where old code took const uint8_t*, size_t. When you see both styles in one file, you are looking at a partially-migrated area — which is also a hint that the file is actively maintained.

The reading skill: distinguishing house style from migration in progress. If you copy the pattern next to your change and it happens to be the old one, a reviewer will ask you to use the new one. Look for the newest code in the file, not the nearest.


Deep dive: reading a Mojo-generated interface without the generated code

You will constantly read code that calls into generated Mojo bindings. The shapes:

mojo::Remote<mojom::blink::FooService> remote_;      // I call the other side
mojo::Receiver<mojom::blink::FooService> receiver_;  // I implement it
mojo::PendingRemote<...> / mojo::PendingReceiver<...>  // an endpoint in transit
mojo::AssociatedRemote<...> / AssociatedReceiver<...>   // shares a pipe: ORDERING preserved

The Associated* variants matter more than their obscurity suggests. Ordinary interfaces get their own message pipes, so messages on two different interfaces have no ordering relationship (bi-02). Associated interfaces share a pipe with a parent interface, which restores ordering.

When you read AssociatedRemote, read it as: "someone was bitten by an ordering bug here."

Note also the mojom::blink:: namespace: Blink gets its own generated variant using WTF types, while the browser side uses mojom:: with STL types. The same .mojom file generates two different C++ APIs, which is why you sometimes find two types with the same name and different string types. Landing on the wrong one is a classic wasted half-hour (bi-01 Technique 3).


Deep dive: a reading checklist for an unfamiliar class

Apply in order; it takes about ten minutes and answers most of the §44 gate.

  1. Is it GarbageCollected? That decides the entire ownership vocabulary.
  2. Read Trace() first — the authoritative list of what it keeps alive.
  3. Scan the member types: Member (graph edge), raw_ptr (non-owning), unique_ptr (owned), scoped_refptr (shared), WeakPtr (may vanish).
  4. Look for SEQUENCE_CHECKER / THREAD_CHECKER — which thread does this belong to?
  5. Read the DCHECKs and CHECKs — the invariants, stated executably.
  6. Find the Create() factory or MakeGarbageCollected call sites — who constructs it, and who therefore owns its lifetime?
  7. Open the _test.cc — the enumerated edge cases.
  8. Only then read method bodies, entering from your actual question.

Steps 2 and 5 answer "what invariant does it maintain" better than any prose you would write, and they are two minutes of work.


Lab

Deliberately small — this module is not where the learning is.

  1. Pick five functions you already read in bi-03/bi-04. For each, write one sentence naming every ownership decision it makes (who owns what, what may die).
  2. Find one base::Unretained in the tree. Determine what guarantees the receiver outlives the callback. If you cannot in ten minutes, note that — say why it was hard.
  3. Find a class with SEQUENCE_CHECKER. Name the sequence it belongs to, and how you know.
  4. Find one raw_ptr<T> member and one Member<T> member. Explain why each is right in its context and what breaks if swapped.
  5. Read one generated binding end to end (bi-05). List every idiom you could not name, then look each up here.

Deliverable: a personal one-page cheat sheet of the idioms you got stuck on. That page is worth more than this module.


Core terminology

TermDefinition
std::unique_ptr<T>Sole ownership; moved, never copied
scoped_refptr<T>Shared ownership via RefCounted<T>
raw_ptr<T>Non-owning member, hardened — converts use-after-free into a crash
base::UnretainedAn explicit promise that the receiver outlives the callback
WeakPtr<T> / WeakPtrFactory<T>Callback silently does nothing if the object died; the safe UI default
OnceCallback / RepeatingCallbackChromium's callback types; Once must be moved, not copied
base::BindOnce / BindRepeatingCallback construction; first bound argument is the receiver
FROM_HERESource-location token attached to posted tasks, for tracing and diagnostics
RAII / Scoped*Constructor does, destructor undoes; often the clearest statement of an invariant
CHECKAssertion that survives in release builds
DCHECKDebug-only assertion; a statement of a developer-facing invariant
SEQUENCE_CHECKERExecutable documentation that an object is touched from one sequence only
SequenceAn ordered series of tasks, not necessarily on one physical thread
SequencedTaskRunnerPosts tasks that run in order, on a sequence
Member<T> / Persistent<T> / WeakMember<T>Oilpan handles — not interchangeable with the above
MiraclePtrThe raw_ptr hardening programme's name
Mojo-generated interfaceThe .mojom-derived C++ you read without reading the generated code

Mental models

Every pointer type is a lifetime contract, stated in the type system. You do not need to read an implementation to know who owns what — that is the point of the vocabulary, and it is why reading Chromium is more tractable than its size suggests.

Two vocabularies, one rule. Oilpan handles inside GarbageCollected Blink classes; //base smart pointers everywhere else. Getting this wrong is the most common newcomer error, and recognising which world a file lives in is a two-second check on the class declaration.

Every Unretained is a claim to verify. It says "I promise this outlives the callback." That promise is sometimes wrong, and when it is, the result is a use-after-free. It is the single highest-yield thing to look at in an unfamiliar file.

CHECK vs DCHECK is a design decision, not a strictness preference. A CHECK survives into release because the invariant is security- or correctness-critical and continuing would be worse than crashing. A DCHECK documents an invariant for developers. Choosing between them is choosing what happens to a user when your assumption is violated in the field — and on the browser side, a violated invariant may be an attack (bi-02).

Scoped* locals encode the invariant more clearly than the surrounding code. When reading an unfamiliar function, they are the "and afterwards this is restored" markers, and they often tell you what the function is really about faster than its body does.

The memory-safety programme explains the aesthetics. raw_ptr's verbosity is a deliberate trade: some syntactic weight in exchange for converting a large class of exploitable bugs into crashes. This is fe-04's "make invalid states difficult to represent," applied in C++ to lifetime.


Common misconceptions

  1. "You need to know C++ to read Chromium." You need the ownership vocabulary, callbacks, threading annotations, and RAII. That subset is small and gets you most of the way.

  2. "raw_ptr is just a raw pointer with a wrapper." It is hardened — the point is that use-after-free becomes a crash rather than an exploit.

  3. "DCHECK and CHECK differ only in build configuration." They differ in what you have decided should happen to a user when the invariant fails in production.

  4. "Oilpan handles and scoped_refptr are both 'the smart pointer.'" They are separate systems. Mixing them is the classic error.

  5. "Unretained is a performance optimisation." It is a lifetime assertion. Treat it as a claim to check, not as a default.

  6. "A sequence is a thread." A sequence is an ordering guarantee; it may migrate across physical threads.

  7. "Threading annotations are comments." SEQUENCE_CHECKER is executable. It will fire.

  8. "You must read the generated Mojo code." You can read the .mojom and the interface's use sites; the generated layer is mechanical.


Interview talking points

  • "Reading Chromium isn't about knowing C++, it's about knowing the subset that answers one question of every pointer: who owns this and how long does it live? Chromium puts that answer in the type, which is why the ownership vocabulary is most of reading comprehension."
  • "When I see a callback I ask what keeps the receiver alive, and the answer is one of four things: ownership, a refptr, a WeakPtr — so it may silently not run — or Unretained, which is an explicit promise. I treat every Unretained as a lifetime claim to verify, because that's where use-after-free lives."
  • "CHECK versus DCHECK is a design decision about what happens to a user when your assumption is violated in the field. On the browser side a violated invariant may be an attack, so crashing is better than continuing — that's the same reasoning as Redux's isDispatching guard, in a language where the consequence is a security bug rather than stale UI."
  • "The two pointer vocabularies — Oilpan handles inside garbage-collected Blink classes, //base smart pointers everywhere else — are the most common newcomer trip-up, and telling which world a file is in takes two seconds on the class declaration."
  • "raw_ptr's verbosity is a deliberate trade: syntactic weight in exchange for turning a large class of exploitable use-after-free bugs into crashes. It's 'make invalid states hard to represent' applied to lifetime, in a language that otherwise won't help you."

Further Reading (primary sources first)

  • docs/callback.md — definitive; read "Introduction" and "Quick reference for basic stuff."
  • docs/threading_and_tasks.md and _faq.md.
  • base/memory/raw_ptr.h — read the header comment for the rationale.
  • third_party/blink/renderer/platform/heap/BlinkGCAPIReference.md — Oilpan handles.
  • styleguide/c++/ in-tree — the Chromium C++ style guide and the allowed-features list. Check it before using any modern C++ feature; Chromium gates them deliberately.
  • third_party/blink/renderer/README.md §"Type dependencies".

Principal Engineer Review

  1. You see base::Unretained(this) in a callback posted to another sequence. What must be true for this to be correct, how would you verify it, and what would you propose instead?

  2. raw_ptr<T> turns some use-after-free bugs into crashes. Argue this is a security improvement; then argue a crash in production is its own outage. How does Chromium's threat model settle it?

  3. Chromium builds without exceptions. What does this simplify, what does it make more verbose, and how are error paths expressed instead?

  4. Blink has two ownership vocabularies (Oilpan and //base). Argue for unifying them. What is the actual obstacle?

  5. AtomicString makes name comparison a pointer compare. What does interning cost, and when is it the wrong choice?

  6. You are reviewing a first-time contributor's CL that adds a raw T* member to a GarbageCollected class. Write the review comment: correct, specific, not discouraging.

  7. Chromium encodes invariants in DCHECK, SEQUENCE_CHECKER, clang plugins and presubmits rather than in comments. Make the case this is better documentation than prose — and name where it fails.

  8. Which parts of modern C++ would you deliberately keep out of a large codebase you owned, and what does your list say about what you optimise for?


Connections to other modules

  • bi-01 — you need this vocabulary to read anything the navigation techniques find.
  • bi-02 — why CHECK survives release on the browser side: a violated invariant may be an attack.
  • bi-04 — the Oilpan half of the pointer vocabulary, in its own context.
  • bi-05 — reading a generated binding without reading the generated code.
  • bi-12 — SEQUENCE_CHECKER and FROM_HERE are what make traces and crash reports legible.
  • bi-14 — the style documents that govern here are the ones your CL will be reviewed against.
  • fw-01 — isDispatching is the same reasoning as CHECK: when a contract violation produces corruption rather than an error, make it an error.
  • fe-04 — "make invalid states difficult to represent," applied to lifetime instead of data.