V8 Engine · Lesson 6 · Embedding Track

Expose a Native Function to JS

Why this lesson: Your mission's capstone embedding skill: "expose a native function to JS and exchange values." So far data flowed one way — your host ran a script and read the result. Now you'll let JavaScript running inside your engine call back into your C++ code. That two-way bridge is what makes embedding useful.

The whole point of embedding is extensibility: you host a scripting environment and expose your application's capabilities to it. In V8 you do that with templates. A FunctionTemplate is, in the guide's words, "the blueprint for a single function", and you can "associate a C++ callback with a function template which is called when the JavaScript function instance is invoked."

The mechanism in one picture

JS calls add(3,4)
→
V8 invokes your C++ callback
→
args[i] reads inputs
→
GetReturnValue().Set() sends result back

The callback signature

Every C++ function you expose has this exact shape. Everything — arguments in, result out, the isolate — arrives through one parameter, args:

void AddCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();

  // Inputs: args.Length() = how many, args[i] = the i-th (a Local<Value>)
  if (args.Length() < 2) return;
  double a = args[0].As<v8::Number>()->Value();
  double b = args[1].As<v8::Number>()->Value();

  // Output: hand a JS value back to the caller
  args.GetReturnValue().Set(v8::Number::New(isolate, a + b));
}
PieceRole
FunctionCallbackInfo<Value>& argsThe one bundle V8 passes in: arguments, the receiver (this), the isolate, and the return-value slot.
args.GetIsolate()The current isolate — you need it to create new JS values.
args.Length() / args[i]Argument count and each argument as a Local<Value>. You convert/check types yourself.
args.GetReturnValue().Set(v)The function's return value. Don't return a value in C++ — set it here.
A logging callback (input only). The classic print from samples/shell.cc follows the same shape — read an arg, do something, return nothing:
void LogCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();
  v8::HandleScope scope(isolate);            // a callback should open its own scope
  v8::String::Utf8Value str(isolate, args[0]);
  printf("[js log] %s\n", *str);
}
Pattern from the Embedder's Guide & samples/shell.cc.

Wiring it into the global object

A callback isn't reachable from JS until you attach it to something. The usual move: build a global ObjectTemplate, put your functions on it as FunctionTemplates, then create the Context with that template — so JS sees add(...) and log(...) as globals.

v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);

// 1. Blueprint for the global object
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate);

// 2. Attach native functions as global properties
global->Set(isolate, "add", v8::FunctionTemplate::New(isolate, AddCallback));
global->Set(isolate, "log", v8::FunctionTemplate::New(isolate, LogCallback));

// 3. Create the context using that global template
v8::Local<v8::Context> context = v8::Context::New(isolate, nullptr, global);
v8::Context::Scope context_scope(context);

// 4. Now JS can call them!
v8::Local<v8::String> src = v8::String::NewFromUtf8Literal(
    isolate, "log('2 + 3 = ' + add(2, 3));");
v8::Script::Compile(context, src).ToLocalChecked()->Run(context).ToLocalChecked();
// prints:  [js log] 2 + 3 = 5
Templates vs instances. A FunctionTemplate/ObjectTemplate is a blueprint, defined once. When you create a Context from the global template, V8 instantiates it into a live global object. This separation lets you set up the shape of your API once and stamp out fresh, isolated instances per Context — the same mechanism Chrome uses to give every frame its own clean globals.

Putting it together hands-on

Take your hello_world.cc from Lesson 4, add the two callbacks above the main function, replace the context creation with the template-wiring block, and swap the script for one that calls add and log. Recompile with the same command from Lesson 4. You'll have a host whose JavaScript can reach into your C++.

This is the seed of a real runtime. Node's fs.readFile, console.log, and every built-in are exactly this pattern at scale: C++ functions exposed to JS via templates, reading args and setting return values. You've now seen the atom the whole thing is built from.

Check yourself feedback loop

1. How does a C++ callback return a value to its JavaScript caller?
2. What's the difference between a FunctionTemplate and the function JS actually calls?
3. Inside a callback, where do the JavaScript arguments and the isolate come from?
4. Why open a HandleScope at the top of a callback like LogCallback?
Ask me anything. Want to expose a whole C++ object to JS (not just functions) using internal fields and accessors — the samples/process.cc pattern? Want to throw a JS exception from C++, or read a JS object's properties inside a callback? Just ask — e.g. "Show me how to wrap a C++ class as a JS object," or "How do I validate arg types and throw a TypeError?"
🎉 You've completed the planned arc. Internals (Lessons 1, 2, 7, 8) and embedding (Lessons 3, 4, 5, 6). You can now trace JS from source to optimized machine code, predict its performance from hidden classes, explain the moving GC, and write a C++ host that runs scripts and exchanges values both ways — every success criterion in your mission.

Where to go next

This was the roadmap, but mastery comes from building. Natural next steps your teacher can spin up on request: wrapping a C++ class as a JS object (process.cc); building a tiny REPL like shell.cc; a deep-dive on TurboFan's sea-of-nodes IR; or testing your understanding in the v8-users community (see RESOURCES.md). Tell me what you want to chase.

V8 Engine learning track · Lesson 6 · Embedding capstone. Patterns from the official Embedder's Guide & samples.