C++ primer for V8 hackers
You do not need to be a C++ expert to read V8, but you must recognize a handful of recurring idioms. V8 is modern C++ (it tracks a specific Clang toolchain) with a heavy dose of macros and hand-rolled abstractions for performance and GC safety. This page is a survival kit; we link to it whenever a snippet leans on one of these.
::: info Why so many custom abstractions?
A garbage collector can move objects at almost any allocation point. That single
fact forces V8 to wrap raw pointers (Handle), forbid storing them across GC
points, and generate object accessors via macros. Most of the idioms below
exist to make GC safety checkable rather than a matter of discipline.
:::
Handles: pointers the GC can move under you
The most important idea: you almost never hold a raw HeapObject*. Because
the GC can move (compact) objects, a raw pointer can become stale. Instead V8
hands you a Handle<T> — a pointer to a slot in a side table that the GC
updates when it moves the object.
Handle<T>/DirectHandle<T>— a GC-safe reference to a heap object.HandleScope— a stack-allocated region; all handles created within it are freed when it goes out of scope. You will seeHandleScope scope(isolate);at the top of nearly every function that touches the heap.Local<T>— the public API equivalent (ininclude/v8.h), scoped to aHandleScopefrom the embedder's side.MaybeHandle<T>/Maybe<T>— a handle (or value) that may be empty because the operation could have thrown. Forces you to handle the exception path.Tagged<T>— a raw tagged value (Smi-or-pointer) used where you are not crossing a GC point and want zero overhead. The newerTagged<T>/DirectHandle<T>types replace older rawObject/HeapObjectusage.
::: warning The cardinal rule
Never store a raw Tagged<HeapObject> across an allocation. If code can trigger
GC between obtaining a pointer and using it, you need a Handle. Violating this
is the classic V8 memory-corruption bug.
:::
The object class macros
V8 objects (Map, JSObject, String, …) are not normal C++ objects with fields.
They are views over tagged memory: an object "is" a tagged pointer, and field
accessors read/write at fixed byte offsets. So you will not find int length_;
member variables — you will find macro-generated accessors like:
DECL_INT_ACCESSORS(length) // declares length() / set_length(int)
DECL_ACCESSORS(elements, Tagged<FixedArrayBase>) // a tagged field accessor
Layout is defined by offset constants (often kHeaderSize, kLengthOffset, …)
or generated by Torque (see below). When you open an *-inl.h file you will
see the bodies of these accessors doing raw reads at those offsets.
::: details The -inl.h split
V8 splits most classes into foo.h (declarations) and foo-inl.h (inline
definitions). The inline file is included only where the hot, inlinable bodies
are needed, which keeps compile times and binary size in check. If a method
"isn't defined" in the .h, look in the matching -inl.h.
:::
Torque-generated classes (.tq → -tq.inc)
Many object layouts are declared in Torque (.tq) files and generate C++:
field offsets, accessors, and verification code. If you see a class inheriting
from TorqueGenerated… or a #include "torque-generated/…-tq-inl.inc", the
field layout you are looking for is defined in the corresponding .tq file, not
in hand-written C++. We cover this in Builtins & Torque.
Tagged vs untagged, and the smell of Smi
In object and compiler code you will constantly see the distinction between a
tagged value (carries V8's low-bit tag; could be a Smi or a pointer) and an
untagged machine value (a raw int32_t, Word32, intptr_t). Converting
between them is explicit: Smi::FromInt(x), Smi::ToInt(obj),
(*obj).ptr(). Misreading a tagged value as untagged (or vice versa) is a
category of bug the type system is designed to prevent. See
Tagged values & Smis.
Zones: region-based allocation
The parser and the optimizing compilers allocate huge numbers of short-lived
nodes. Doing that with new/delete would be slow and fragmenting, so V8 uses
Zone allocators: a zone is a region you allocate into with a pointer bump,
and free all at once by destroying the zone. You will see Zone* zone threaded
through the parser, AST, and TurboFan. We revisit zones in
Scanner, parser & AST.
Macros you will see everywhere
V8_INLINE/V8_NOINLINE— force/forbid inlining.V8_WARN_UNUSED_RESULT— the result (often aMaybe) must be checked.DCHECK(...),CHECK(...),DCHECK_EQ(...)— debug-only and always-on assertions.DCHECKs document invariants; reading them is a fast way to learn what a function assumes.USE(x)— silence "unused variable" without side effects.RETURN_ON_EXCEPTION,ASSIGN_RETURN_ON_EXCEPTION— macros that propagate the pending exception out of a function returningMaybe/MaybeHandle.friend+OBJECT_CONSTRUCTORS_IMPL— boilerplate that wires up the object view classes.
Reading tip: start from the DCHECKs and the header
When a function is dense, two anchors orient you fast:
The header comment and declaration state the contract.
The
DCHECKs at the top state the preconditions in code.
Together they tell you what must be true on entry, which is usually enough to follow the body. You will use this trick constantly as you read the source linked throughout V8pedia.