'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.
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:
| Object | What it is (from the Embedder's Guide) |
|---|---|
| v8::Platform | Provides 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.
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().
.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.
*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.
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
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.
V8::Initialize()?Script::Compile() returns a MaybeLocal, and the sample calls .ToLocalChecked(). What does that signify?-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?"
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)