In this part of the series, we will embed V8 into a C++ program, expose a function from C++ to JavaScript, and execute it. By the end of this tutorial, you’ll understand how to run JavaScript inside a C++ program using V8 and expose native C++ functions to JavaScript.
By doing this, we take our first step in building a custom JavaScript runtime, just like Node.js does.

Step 1: Install V8
To get started, install V8 on your system and build it for embedding. You can either,
- Refer to the official V8 embedding documentation: V8 Embed Docs.
- Use a package manager (much easier) like Homebrew (macOS) or APT (Ubuntu/Debian).
Install V8 on macOS (via Homebrew):
brew install v8
Install V8 on Ubuntu/Debian:
sudo apt update
sudo apt install libv8-dev
Once installed, we can verify the setup by running a simple C++ program using V8.
Step 2: Running a Simple JavaScript Script in V8
We’ll start with a minimal C++ program, hello.cpp
that runs JavaScript inside a V8 environment:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "libplatform/libplatform.h"
#include "v8-context.h"
#include "v8-initialization.h"
#include "v8-isolate.h"
#include "v8-local-handle.h"
#include "v8-primitive.h"
#include "v8-script.h"
int main(int argc, char *argv[])
{
// Initialize V8.
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();
// Create a new Isolate and make it the current one.
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);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
// Enter the context for compiling and running the hello world script.
v8::Context::Scope context_scope(context);
{
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source =
v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");
// Compile the source code.
v8::Local<v8::Script> script =
v8::Script::Compile(context, source).ToLocalChecked();
// Run the script to get the result.
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
// Convert the result to an UTF8 string and print it.
v8::String::Utf8Value utf8(isolate, result);
printf("%s\n", *utf8);
}
}
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::DisposePlatform();
delete create_params.array_buffer_allocator;
return 0;
}
Explanation:
- We initialize V8 by setting up ICU, startup data, and a platform.
- We create a new V8 Isolate, which is a separate instance of V8 where JavaScript code runs.
- Inside the isolate, we create a new context, which acts as a global scope for JavaScript execution.
- We compile and run the JavaScript string
'Hello' + ', World!'
and print the result.
Compilation and Execution
macOS (Homebrew V8)
g++ hello.cpp -std=c++20 -I$(brew --prefix v8)/include -L$(brew --prefix v8)/lib -lv8 -lv8_libplatform -DV8_COMPRESS_POINTERS -DV8_ENABLE_SANDBOX -o hello
Ubuntu/Debian
g++ hello.cpp -std=c++20 -lv8 -o hello
To execute
./hello
You should see “Hello, World!” in the console

Note: If you encounter any errors, try using ChatGPT to help debug them.

Step 3: Exposing a C++ Function to JavaScript
Now, we’ll modify our program to expose a C++ function to JavaScript. We’ll create a C++ function called, PrintFunction
, map it to a JS function called logMessage
and then use it in a JS script to log a message to the console.
Here’s what the C++ code of my_nodejs.cpp
looks like,
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "libplatform/libplatform.h"
#include "v8-context.h"
#include "v8-initialization.h"
#include "v8-isolate.h"
#include "v8-local-handle.h"
#include "v8-primitive.h"
#include "v8-script.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include "v8-object.h"
#include "v8-function.h"
using namespace v8;
// C++ function exposed to JavaScript
void PrintFunction(const FunctionCallbackInfo<Value> &args)
{
Isolate *isolate = args.GetIsolate();
String::Utf8Value str(isolate, args[0]);
std::cout << *str << std::endl;
}
// Function to read JavaScript file into a string
std::string ReadFile(const std::string &filename)
{
std::ifstream file(filename);
if (!file)
{
std::cerr << "Error: Cannot open file " << filename << std::endl;
return "";
}
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
int main(int argc, char *argv[])
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <script.js>" << std::endl;
return 1;
}
// Read the JavaScript file
std::string script_content = ReadFile(argv[1]);
if (script_content.empty())
return 1;
// Initialize V8.
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();
// Create a new Isolate and make it the current one.
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);
// Create a stack-allocated handle scope.
v8::HandleScope handle_scope(isolate);
// Create a global object template
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
// Map the JavaScript function logMessage to the C++ function PrintFunction
// global->Set sets the global object in V8, effectively making the JavaScript function logMessage available in the global scope.
global->Set(String::NewFromUtf8(isolate, "logMessage").ToLocalChecked(),
FunctionTemplate::New(isolate, PrintFunction));
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate, NULL, global);
// Enter the context before executing any scripts
v8::Context::Scope context_scope(context);
// Compile and run the script
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, script_content.c_str()).ToLocalChecked();
v8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();
script->Run(context).ToLocalChecked(); // Ensure execution is checked
}
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::DisposePlatform();
delete create_params.array_buffer_allocator;
return 0;
}
Explanation:
- The function
ReadFile
reads a JavaScript file into a string so that it can be executed by V8. - The function
PrintFunction
acts as a bridge between JavaScript and C++. - In line 76, the global object is modified to include
logMessage
, which maps toPrintFunction
. This makes thelogMessage
function available in the global scope of the JS script.
Compile and build my_node.js.cpp using earlier instructions. You’ll obtain an executable, my_node
Now we’ll create a JS script, script.js
to use the logMessage
function,
logMessage(`Embedded V8!`) // This function maps to PrintFunction in my_nodejs.cpp
Execute my_nodejs, ./my_nodejs script.js
. You should see Embedded V8!
printed to the console.

Conclusion
In this part, we successfully embedded V8 in a C++ program, exposed a C++ function to JavaScript, and executed it. In the next part, we’ll create a function to read file contents using JavaScript within our custom runtime.
We’ll start with synchronous file I/O to keep things simple before exploring asynchronous behaviour later. Hasta la próxima!