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:
| Type | Meaning |
|---|---|
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
Unretainedas 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
| Capability | Evidence you have it |
|---|---|
| Read ownership from types | State a pointer's lifetime contract without reading the implementation |
| Audit a callback | Name what keeps the receiver alive, and flag every Unretained |
| Read threading intent | Use SEQUENCE_CHECKER and task runners as the concurrency documentation |
| Read a class cold | Apply 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."
| Type | Meaning | Where |
|---|---|---|
std::unique_ptr<T> | sole ownership; moved, never copied | everywhere 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-free | member fields in non-GC code |
T* | non-owning, no guarantees | locals, parameters |
T& | non-owning, non-null | parameters 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.OnceCallbackruns 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 everyUnretainedas 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::HashSetin Blink;base::flat_map,base::span,std::optionalelsewhere.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:
- Match the file you are editing. Style is enforced by
git cl format— run it and stop thinking about formatting. - Correct handle type. In Blink GC classes,
Member<T>for owned graph edges. Getting this wrong is the most likely substantive review comment. DCHECKyour assumptions. Adding aDCHECKstating your new invariant is usually welcomed, and it is how you document intent in a codebase that discourages comments restating code.- Tests in the right style. Blink
foo_test.cc, Chromiumfoo_unittest.cc. Copy the nearest existing test's structure exactly. - 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:
| Document | Size | What it governs |
|---|---|---|
c++-features.md | ~78 KB | which C++ language/library features are allowed, banned, or under discussion |
c++.md | ~20 KB | the general Chromium C++ style |
c++-dos-and-donts.md | ~18 KB | accumulated guidance and anti-patterns |
blink-c++.md | ~8 KB | Blink-specific rules on top |
const.md, checks.md | const 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.
The Blink-specific rules
blink-c++.md is short and its section headings are the whole message:
- Prefer WTF types over STL and base types
- Do not use
newanddelete - 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.
| Macro | Retained 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_CHECK | staged | a 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:
- A
CHECKis 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. DUMP_WILL_BE_CHECKis a migration in progress — someone wants this to be aCHECKbut 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.
| Mechanism | What 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 |
| PartitionAlloc | the allocator, with partitioning that makes some exploitation harder |
| Rust interop | new, isolated, untrusted-input parsers written in a safe language |
The Rule of Two (bi-02) | the architectural constraint that drives sandboxing decisions |
| Clang plugins | mechanical 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.
- Is it
GarbageCollected? That decides the entire ownership vocabulary. - Read
Trace()first — the authoritative list of what it keeps alive. - Scan the member types:
Member(graph edge),raw_ptr(non-owning),unique_ptr(owned),scoped_refptr(shared),WeakPtr(may vanish). - Look for
SEQUENCE_CHECKER/THREAD_CHECKER— which thread does this belong to? - Read the
DCHECKs andCHECKs — the invariants, stated executably. - Find the
Create()factory orMakeGarbageCollectedcall sites — who constructs it, and who therefore owns its lifetime? - Open the
_test.cc— the enumerated edge cases. - 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.
- 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).
- Find one
base::Unretainedin the tree. Determine what guarantees the receiver outlives the callback. If you cannot in ten minutes, note that — say why it was hard. - Find a class with
SEQUENCE_CHECKER. Name the sequence it belongs to, and how you know. - Find one
raw_ptr<T>member and oneMember<T>member. Explain why each is right in its context and what breaks if swapped. - 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
| Term | Definition |
|---|---|
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::Unretained | An 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 / RepeatingCallback | Chromium's callback types; Once must be moved, not copied |
base::BindOnce / BindRepeating | Callback construction; first bound argument is the receiver |
FROM_HERE | Source-location token attached to posted tasks, for tracing and diagnostics |
RAII / Scoped* | Constructor does, destructor undoes; often the clearest statement of an invariant |
CHECK | Assertion that survives in release builds |
DCHECK | Debug-only assertion; a statement of a developer-facing invariant |
SEQUENCE_CHECKER | Executable documentation that an object is touched from one sequence only |
| Sequence | An ordered series of tasks, not necessarily on one physical thread |
SequencedTaskRunner | Posts tasks that run in order, on a sequence |
Member<T> / Persistent<T> / WeakMember<T> | Oilpan handles — not interchangeable with the above |
| MiraclePtr | The raw_ptr hardening programme's name |
| Mojo-generated interface | The .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
-
"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.
-
"
raw_ptris just a raw pointer with a wrapper." It is hardened — the point is that use-after-free becomes a crash rather than an exploit. -
"
DCHECKandCHECKdiffer only in build configuration." They differ in what you have decided should happen to a user when the invariant fails in production. -
"Oilpan handles and
scoped_refptrare both 'the smart pointer.'" They are separate systems. Mixing them is the classic error. -
"
Unretainedis a performance optimisation." It is a lifetime assertion. Treat it as a claim to check, not as a default. -
"A sequence is a thread." A sequence is an ordering guarantee; it may migrate across physical threads.
-
"Threading annotations are comments."
SEQUENCE_CHECKERis executable. It will fire. -
"You must read the generated Mojo code." You can read the
.mojomand 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 — orUnretained, which is an explicit promise. I treat everyUnretainedas a lifetime claim to verify, because that's where use-after-free lives." - "
CHECKversusDCHECKis 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'sisDispatchingguard, 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,
//basesmart 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.mdand_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
-
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? -
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? -
Chromium builds without exceptions. What does this simplify, what does it make more verbose, and how are error paths expressed instead?
-
Blink has two ownership vocabularies (Oilpan and
//base). Argue for unifying them. What is the actual obstacle? -
AtomicStringmakes name comparison a pointer compare. What does interning cost, and when is it the wrong choice? -
You are reviewing a first-time contributor's CL that adds a raw
T*member to aGarbageCollectedclass. Write the review comment: correct, specific, not discouraging. -
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. -
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— whyCHECKsurvives 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_CHECKERandFROM_HEREare 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—isDispatchingis the same reasoning asCHECK: 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.