Skip to content

Instantly share code, notes, and snippets.

@surusek
Last active June 20, 2024 18:07
Show Gist options
  • Save surusek/4c05e4dcac6b82d18a1a28e6742fc23e to your computer and use it in GitHub Desktop.
Save surusek/4c05e4dcac6b82d18a1a28e6742fc23e to your computer and use it in GitHub Desktop.
V8 module importing - simple example

Maintenance (or lack of it)

This is a not great piece of code I've wrote few years ago (I didn't have better things to do when I was 17, apperantly), when I was fiddling around with the V8 JS Engine. It doesn't work with newer versions of V8, since the library doesn't have a stable API. Time, where I had time to fight with the depot_tools and lackluster MSVC support for fun is long gone. I've tried to redo this example once in the past, after I've got an email notification that someone got interested in stuff that I've put on the net and have forgotten about. Toolset got even more picky than I remember it being and my attention for personal programming projects drifted away somewhere else, so it's highly unlikely that I'll update it to the newer API. But I'm leaving the code there, maybe someone will make good use of it.

// from https://v8.dev/features/modules
export const repeat = (string) => `${string} ${string}`;
export function shout(string) {
return `${string.toUpperCase()}!`;
}
/*
* Google V8 JavaScript module importing example
* by surusek <almostnoruby@live.com>
* ---
* based on https://stackoverflow.com/a/52031275,
* https://chromium.googlesource.com/v8/v8/+/master/samples/hello-world.cc and
* https://gist.github.com/bellbind/b69c3aa266cffe43940c
* ---
* This code snippet is distributed in public domain, so you can do anything
* you want with it.
* ---
* This program takes filename from command line and executes that file as
* JavaScript code, loading modules from other files, if nessesary. Since it is
* only a simple example, all files specified by import are loaded from root
* directory of the program, e.g. if module "foo/bar.mjs" is imported, and it
* imports "baz.mjs", it doesn't import "foo/baz.mjs", but "baz.mjs".
*/
/*****************************************************************************
* Includes
*****************************************************************************/
// Including V8 libraries
#include <libplatform/libplatform.h>
#include <v8.h>
// printf(), exit()
#include <stdio.h>
#include <stdlib.h>
// File operations
#include <fstream>
/*****************************************************************************
* Declarations
*****************************************************************************/
// Reads a file to char array; line #140
char* readFile(char filename[]);
// Simple print function binding to JavaScript VM; line #169
void print(const v8::FunctionCallbackInfo<v8::Value>& args);
// Loads a module; line #187
v8::MaybeLocal<v8::Module> loadModule(char code[],
char name[],
v8::Local<v8::Context> cx);
// Check, if module isn't empty (or pointer to it); line #221
v8::Local<v8::Module> checkModule(v8::MaybeLocal<v8::Module> maybeModule,
v8::Local<v8::Context> cx);
// Executes module; line #247
v8::Local<v8::Value> execModule(v8::Local<v8::Module> mod,
v8::Local<v8::Context> cx,
bool nsObject = false);
// Callback for static import; line #270
v8::MaybeLocal<v8::Module> callResolve(v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,
v8::Local<v8::Module> referrer);
// Callback for dynamic import; line #285
v8::MaybeLocal<v8::Promise> callDynamic(v8::Local<v8::Context> context,
v8::Local<v8::ScriptOrModule> referrer,
v8::Local<v8::String> specifier);
// Callback for module metadata; line #310
void callMeta(v8::Local<v8::Context> context,
v8::Local<v8::Module> module,
v8::Local<v8::Object> meta);
/*****************************************************************************
* int main
* Application entrypoint.
*****************************************************************************/
int main(int argc, char* argv[]) {
// Since program has to parse filename as argument, here is check for that
if (!(argc == 2)) {
printf(
"No argument provided or too much arguments provided! Enter "
"filename as first one to continue\n");
exit(EXIT_FAILURE);
}
// Where is icudtxx.dat? Does nothing if ICU database is in library itself
v8::V8::InitializeICUDefaultLocation(argv[0]);
// Where is snapshot_blob.bin? Does nothing if external data is disabled
v8::V8::InitializeExternalStartupData(argv[0]);
// Creating platform
std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
// Initializing V8 VM
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();
// Creating isolate from the params (VM instance)
v8::Isolate::CreateParams mCreateParams;
mCreateParams.array_buffer_allocator =
v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* mIsolate;
mIsolate = v8::Isolate::New(mCreateParams);
// Binding dynamic import() callback
mIsolate->SetHostImportModuleDynamicallyCallback(callDynamic);
// Binding metadata loader callback
mIsolate->SetHostInitializeImportMetaObjectCallback(callMeta);
{
// Initializing handle scope
v8::HandleScope handle_scope(mIsolate);
// Binding print() funtion to the VM; check line #
v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(mIsolate);
global->Set(mIsolate, "print", v8::FunctionTemplate::New(mIsolate, print));
// Creating context
v8::Local<v8::Context> mContext = v8::Context::New(mIsolate, nullptr, global);
v8::Context::Scope context_scope(mContext);
{
// Reading a module from file
char* contents = readFile(argv[1]);
// Executing module
v8::Local<v8::Module> mod =
checkModule(loadModule(contents, argv[1], mContext), mContext);
v8::Local<v8::Value> returned = execModule(mod, mContext);
// Returning module value to the user
v8::String::Utf8Value val(mIsolate, returned);
printf("Returned value: %s\n", *val);
}
}
// Proper VM deconstructing
mIsolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete mCreateParams.array_buffer_allocator;
}
/*****************************************************************************
* char* readFile
* Reads file contents to a null-terminated string.
*****************************************************************************/
char* readFile(char filename[]) {
// Opening file; ifstream::ate is use to determine file size
std::ifstream file;
file.open(filename, std::ifstream::ate);
char* contents;
if (!file) {
contents = new char[1];
return contents;
}
// Get file size
size_t file_size = file.tellg();
// Return file pointer from end of the file (set by ifstream::ate) to beginning
file.seekg(0);
// Reading file to char array and returing it
std::filebuf* file_buf = file.rdbuf();
contents = new char[file_size + 1]();
file_buf->sgetn(contents, file_size);
file.close();
return contents;
}
/*****************************************************************************
* void print
* Binding of simple console print function to the VM
*****************************************************************************/
void print(const v8::FunctionCallbackInfo<v8::Value>& args) {
// Getting arguments; error handling
v8::Isolate* isolate = args.GetIsolate();
v8::String::Utf8Value val(isolate, args[0]);
if (*val == nullptr)
isolate->ThrowException(
v8::String::NewFromUtf8(isolate, "First argument of function is empty")
.ToLocalChecked());
// Printing
printf("%s\n", *val);
}
/*****************************************************************************
* v8::MaybeLocal<v8::Module> loadModule
* Loads module from code[] without checking it
*****************************************************************************/
v8::MaybeLocal<v8::Module> loadModule(char code[],
char name[],
v8::Local<v8::Context> cx) {
// Convert char[] to VM's string type
v8::Local<v8::String> vcode =
v8::String::NewFromUtf8(cx->GetIsolate(), code).ToLocalChecked();
// Create script origin to determine if it is module or not.
// Only first and last argument matters; other ones are default values.
// First argument gives script name (useful in error messages), last
// informs that it is a module.
v8::ScriptOrigin origin(
v8::String::NewFromUtf8(cx->GetIsolate(), name).ToLocalChecked(),
v8::Integer::New(cx->GetIsolate(), 0),
v8::Integer::New(cx->GetIsolate(), 0), v8::False(cx->GetIsolate()),
v8::Local<v8::Integer>(), v8::Local<v8::Value>(),
v8::False(cx->GetIsolate()), v8::False(cx->GetIsolate()),
v8::True(cx->GetIsolate()));
// Compiling module from source (code + origin)
v8::Context::Scope context_scope(cx);
v8::ScriptCompiler::Source source(vcode, origin);
v8::MaybeLocal<v8::Module> mod;
mod = v8::ScriptCompiler::CompileModule(cx->GetIsolate(), &source);
// Returning non-checked module
return mod;
}
/*****************************************************************************
* v8::Local<v8::Module> checkModule
* Checks out module (if it isn't nullptr/empty)
*****************************************************************************/
v8::Local<v8::Module> checkModule(v8::MaybeLocal<v8::Module> maybeModule,
v8::Local<v8::Context> cx) {
// Checking out
v8::Local<v8::Module> mod;
if (!maybeModule.ToLocal(&mod)) {
printf("Error loading module!\n");
exit(EXIT_FAILURE);
}
// Instantianing (including checking out depedencies). It uses callResolve
// as callback: check #
v8::Maybe<bool> result = mod->InstantiateModule(cx, callResolve);
if (result.IsNothing()) {
printf("\nCan't instantiate module.\n");
exit(EXIT_FAILURE);
}
// Returning check-out module
return mod;
}
/*****************************************************************************
* v8::Local<v8::Value> execModule
* Executes module's code
*****************************************************************************/
v8::Local<v8::Value> execModule(v8::Local<v8::Module> mod,
v8::Local<v8::Context> cx,
bool nsObject) {
// Executing module with return value
v8::Local<v8::Value> retValue;
if (!mod->Evaluate(cx).ToLocal(&retValue)) {
printf("Error evaluating module!\n");
exit(EXIT_FAILURE);
}
// nsObject determins, if module namespace or return value has to be returned.
// Module namespace is required during import callback; see lines # and #.
if (nsObject)
return mod->GetModuleNamespace();
else
return retValue;
}
/*****************************************************************************
* v8::MaybeLocal<v8::Module> callResolve
* Callback from static import.
*****************************************************************************/
v8::MaybeLocal<v8::Module> callResolve(v8::Local<v8::Context> context,
v8::Local<v8::String> specifier,
v8::Local<v8::Module> referrer) {
// Get module name from specifier (given name in import args)
v8::String::Utf8Value str(context->GetIsolate(), specifier);
// Return unchecked module
return loadModule(readFile(*str), *str, context);
}
/*****************************************************************************
* v8::MaybeLocal<v8::Promise> callDynamic
* Callback from dynamic import.
*****************************************************************************/
v8::MaybeLocal<v8::Promise> callDynamic(v8::Local<v8::Context> context,
v8::Local<v8::ScriptOrModule> referrer,
v8::Local<v8::String> specifier) {
// Promise resolver: that way promise for dynamic import can be rejected
// or full-filed
v8::Local<v8::Promise::Resolver> resolver =
v8::Promise::Resolver::New(context).ToLocalChecked();
v8::MaybeLocal<v8::Promise> promise(resolver->GetPromise());
// Loading module (with checking)
v8::String::Utf8Value name(context->GetIsolate(), specifier);
v8::Local<v8::Module> mod =
checkModule(loadModule(readFile(*name), *name, context), context);
v8::Local<v8::Value> retValue = execModule(mod, context, true);
// Resolving (fulfilling) promise with module global namespace
resolver->Resolve(context, retValue);
return promise;
}
/*****************************************************************************
* void callMeta
* Callback for module metadata.
*****************************************************************************/
void callMeta(v8::Local<v8::Context> context,
v8::Local<v8::Module> module,
v8::Local<v8::Object> meta) {
// In this example, this is throw-away function. But it shows that you can
// bind module's url. Here, placeholder is used.
meta->Set(
context,
v8::String::NewFromUtf8(context->GetIsolate(), "url").ToLocalChecked(),
v8::String::NewFromUtf8(context->GetIsolate(), "https://something.sh")
.ToLocalChecked());
}
// Static import
import {repeat, shout} from './lib.mjs';
let r = repeat('hello');
let s = shout('Modules in action');
print(r);
print(s);
// Dynamic import
import('./lib.mjs')
.then((module) => {
let a = module.repeat('hello');
let b = module.shout ('Modules in action');
print(a);
print(b);
});
@EndoSakura
Copy link

very good!!!

@ulvimemmeedov
Copy link

thank for example I get this error App.js line 1: SyntaxError: Cannot use import statement outside a module
import modul from './modu.js';
^^^^^^

@surusek
Copy link
Author

surusek commented Feb 1, 2022

I'm not working with V8 much recently, but if I'm reading docs correctly, the v8::ScriptOrigin constructor has been changed: https://v8.github.io/api/head/classv8_1_1ScriptOrigin.html#a8ecfec138674d65f7f6c9101da09092a
Make sure that the is_module argument is set to true.

@ulvimemmeedov
Copy link

ScriptOrigin origin(String::NewFromUtf8(this->GetIsolate(), filename, NewStringType::kNormal).ToLocalChecked());

this is my script origin constructor is module how can i send parameter?

@Aurailus
Copy link

How would someone implement relative imports using something like this?

@surusek
Copy link
Author

surusek commented Mar 26, 2022

@Aurailus With C++17 you can use the resource_name field in the constructor from script origin to store absolute path to the file and let std::filesystem do the magic.

(I really need to write a better example at some point in the future...)

@ezracelli
Copy link

Do you have any pointers on how to handle top-level await — or driving promise execution in general?

In the execModule function, the retValue variable will always be a v8::Local<v8::Promise>. If there’s no top-level await-ing happening, this promise’s .State() will either be kFulfilled or kRejected; if there is top-level await-ing, it could be kPending.

What’s the best way to wait for the promise to settle?

const mod = await import('./lib.mjs');

let a = module.repeat('hello');
let b = module.shout('Modules in action');

print(a);
print(b);

@mohsenomidi
Copy link

is there any update for new version of V8?
I am trying to compile but I got error for callDynamic the function is totally changed HostImportModuleDynamicallyCallback and also ScriptOrigin has been changed ScriptOrigin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment