V8 Engine · Lesson 4 · Embedding Track

The Hello-World Embedder

Why this lesson: Your mission's core embedding goal: "write a C++ host that creates an Isolate + Context, runs a script, and exchange values." This is the first half — the minimal program that boots its own V8, runs 'Hello' + ', World!', and prints the result.

An embedder is any C++ program that hosts V8 (Chrome and Node are just very large embedders). The official samples/hello-world.cc is the smallest possible one. Let's read it as a story in four nested layers, then compile and run it on the V8 you built in Lesson 3.

The shape of every embedder: nested scopes

Before the code, the mental model. An embedder is a set of nested setups, each one a C++ object whose constructor enters something and whose destructor leaves it (that's the RAII idiom — "Resource Acquisition Is Initialization"; the object's lifetime is the resource's lifetime). The nesting, outer to inner:

Platform + V8::Initialize() process-wide, once
Isolate — an isolated VM instance with its own heap v8::Isolate::Scope
HandleScope — container for all your Local handles v8::HandleScope
Context — an isolated JS execution environment (its own globals) v8::Context::Scope
Compile & run your script here the actual work
ObjectWhat it is (from the Embedder's Guide)
v8::PlatformProvides threading/task primitives V8 needs. Created with NewDefaultPlatform() and registered before V8::Initialize().
v8::Isolate"An isolate is a VM instance with its own heap." One running thread at a time. Objects from one isolate can't be used in another.
v8::HandleScope"A container for any number of handles" — delete the scope and all its Local handles go at once. Must be stack-allocated.
v8::Context"An execution environment that allows separate, unrelated JavaScript code to run in a single instance of V8." Its own globalThis, built-ins, etc.
v8::Local<T>A handle (GC-safe pointer) whose lifetime is tied to the enclosing HandleScope. The default handle you use everywhere.

All definitions quoted from v8.dev/docs/embed.

The code (the JS-running core of hello-world.cc)

This is the current official sample, trimmed to the JavaScript path (the real file also runs a WebAssembly snippet — same pattern). Read the comments top to bottom:

// modern V8 uses granular headers, not a single v8.h
#include "include/libplatform/libplatform.h"
#include "include/v8-context.h"
#include "include/v8-initialization.h"
#include "include/v8-isolate.h"
#include "include/v8-local-handle.h"
#include "include/v8-primitive.h"
#include "include/v8-script.h"

int main(int argc, char* argv[]) {
  // 1. Process-wide init — do this once. Order matters: platform BEFORE Initialize.
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();

  // 2. Create an Isolate (a VM instance with its own heap).
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator =
      v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  v8::Isolate* isolate = v8::Isolate::New(create_params);
  {
    v8::Isolate::Scope isolate_scope(isolate);   // make this isolate current
    v8::HandleScope handle_scope(isolate);       // 3. container for Local handles (stack-only)

    // 4. Create and enter a Context (an isolated JS environment).
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    v8::Context::Scope context_scope(context);
    {
      // 5. The actual work: source -> compile -> run -> read result.
      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();

      // 6. Convert the JS string result to UTF-8 and print it.
      v8::String::Utf8Value utf8(isolate, result);
      printf("%s\n", *utf8);              // => Hello, World!
    }
  }

  // 7. Tear down in reverse order.
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::DisposePlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

Verbatim structure from samples/hello-world.cc (current main). Note modern V8 uses DisposePlatform(), not the older ShutdownPlatform().

Three things worth pausing on

.ToLocalChecked() — V8's "this can fail" pattern. Compile and Run return a MaybeLocal<T>, not a Local<T>, because JS can throw. ToLocalChecked() says "I assert this succeeded — crash if not." In real code you'd check for the empty case and read the exception instead. It's V8's way of forcing you to acknowledge that running untrusted JS can fail.
Why *utf8 with a star? v8::String::Utf8Value is a small RAII wrapper that converts a JS string into a C string buffer and frees it in its destructor. Its operator* hands you the char* inside. Classic C++ resource-wrapper idiom — the same pattern as the scopes.

Compile and run it hands-on

1From your V8 checkout root (~/v8/v8), with the monolith built in Lesson 3, compile the sample against it:

cd ~/v8/v8
clang++ -I. -Iinclude samples/hello-world.cc -o hello_world \
  -fno-rtti -std=c++20 \
  -DV8_COMPRESS_POINTERS -DV8_ENABLE_SANDBOX \
  -Lout.gn/arm64.release.sample/obj/ \
  -lv8_monolith -lv8_libbase -lv8_libplatform \
  -ldl -pthread

./hello_world

Expected output:

Hello, World!
3 + 4 = 7        # from the WebAssembly part of the sample
Flag notes (macOS, verified against the embed doc). The doc's command targets Linux; on macOS: use clang++ (invoking g++ runs Apple Clang anyway); you can drop -fuse-ld=lld unless you have LLD installed. The defines -DV8_COMPRESS_POINTERS -DV8_ENABLE_SANDBOX and -std=c++20 are required — they must match how the .sample library was built, or you'll get link errors. Keep the -L path matching your build dir (out.gn/… from v8gen.py). Source: v8.dev/docs/embed.

Check yourself feedback loop

1. What's the difference between an Isolate and a Context?
2. Why must the platform be created and registered before V8::Initialize()?
3. Script::Compile() returns a MaybeLocal, and the sample calls .ToLocalChecked(). What does that signify?
Ask me anything. Link errors are the rite of passage here — paste them and I'll diagnose (usually a mismatched define or -L path). Want to modify the script to run your own JS and read back a number instead of a string? Ask: "Help me change it to evaluate 40+2 and print it as an int," or "What goes wrong if I forget the HandleScope?"

Where this lands you

You've compiled a real embedder and driven V8 from C++ — boot, context, compile, run, read result, tear down. But we glossed over one thing: those Local<T> handles, and why every value is wrapped in one. Lesson 5 goes deep on handles and scopes — Local vs Global, and the GC reason they're mandatory (which Lesson 8 already spoiled: the heap moves).

Reference: V8 Glossary (embedding terms) · Related: Lesson 8 (why handles)

V8 Engine learning track · Lesson 4 · Code quoted from the official samples/hello-world.cc.