V8pedia

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 see HandleScope scope(isolate); at the top of nearly every function that touches the heap.

  • Local<T> — the public API equivalent (in include/v8.h), scoped to a HandleScope from 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 newer Tagged<T> / DirectHandle<T> types replace older raw Object/HeapObject usage.

::: 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 a Maybe) 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 returning Maybe/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:

  1. The header comment and declaration state the contract.

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