Skip to content

Commit

Permalink
feat: preload function for Environment
Browse files Browse the repository at this point in the history
This PR provides an API in node::Environment to allow embedders to set a
preload function for environment, which will run after the environment is
loaded and before the main script runs.

This is similiar to the --require CLI option, but runs a C++ function,
and can only be set by embedders.

The preload function can be used by embedders to inject scripts before
running the main script, for example:
1. In Electron it is used to initialize the ASAR virtual filesystem,
   inject custom process properties, etc.
2. In VS Code it can be used to reset the locations where an extension
   can search for modules.
  • Loading branch information
zcbenz committed Jan 22, 2024
1 parent 27d839f commit 66f52f5
Show file tree
Hide file tree
Showing 7 changed files with 48 additions and 0 deletions.
7 changes: 7 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ function setupUserModules(forceDefaultLoader = false) {
initializeESMLoader(forceDefaultLoader);
const CJSLoader = require('internal/modules/cjs/loader');
assert(!CJSLoader.hasLoadedAnyUserCJSModule);
if (getEmbedderOptions().hasEmbedderPreload) {
runEmbedderPreload();
}
// Do not enable preload modules if custom loaders are disabled.
// For example, loader workers are responsible for doing this themselves.
// And preload modules are not supported in ShadowRealm as well.
Expand Down Expand Up @@ -754,6 +757,10 @@ function initializeFrozenIntrinsics() {
}
}

function runEmbedderPreload() {
internalBinding('mksnapshot').runEmbedderPreload(process, require);
}

function loadPreloadModules() {
// For user code, we preload modules if `-r` is passed
const preloadModules = getOptionValue('--require');
Expand Down
8 changes: 8 additions & 0 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ inline void Environment::set_embedder_entry_point(StartExecutionCallback&& fn) {
embedder_entry_point_ = std::move(fn);
}

inline const EmbedderPreloadCallback& Environment::embedder_preload() const {
return embedder_preload_;
}

inline void Environment::set_embedder_preload(EmbedderPreloadCallback&& fn) {
embedder_preload_ = std::move(fn);
}

inline double Environment::new_async_id() {
async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter] += 1;
return async_hooks()->async_id_fields()[AsyncHooks::kAsyncIdCounter];
Expand Down
15 changes: 15 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,11 @@ void DefaultProcessExitHandlerInternal(Environment* env, ExitCode exit_code);
v8::Maybe<ExitCode> SpinEventLoopInternal(Environment* env);
v8::Maybe<ExitCode> EmitProcessExitInternal(Environment* env);

using EmbedderPreloadCallback =
std::function<void(Environment* env,
v8::Local<v8::Value> process,
v8::Local<v8::Value> require)>;

/**
* Environment is a per-isolate data structure that represents an execution
* environment. Each environment has a principal realm. An environment can
Expand Down Expand Up @@ -1002,6 +1007,15 @@ class Environment : public MemoryRetainer {
inline const StartExecutionCallback& embedder_entry_point() const;
inline void set_embedder_entry_point(StartExecutionCallback&& fn);

// Set a preload function that will run before executing the entry point, this
// is usually used by embedders to inject scripts.
// The function is executed with preload(process, require), and the passed
// |require| function has access to internal Node.js modules.
// The preload function is inherited by worker threads and thus will run in
// work threads, so make sure the function is thread-safe.
inline const EmbedderPreloadCallback& embedder_preload() const;
inline void set_embedder_preload(EmbedderPreloadCallback&& fn);

inline void set_process_exit_handler(
std::function<void(Environment*, ExitCode)>&& handler);

Expand Down Expand Up @@ -1208,6 +1222,7 @@ class Environment : public MemoryRetainer {

builtins::BuiltinLoader builtin_loader_;
StartExecutionCallback embedder_entry_point_;
EmbedderPreloadCallback embedder_preload_;

// Used by allocate_managed_buffer() and release_managed_buffer() to keep
// track of the BackingStore for a given pointer.
Expand Down
6 changes: 6 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1304,6 +1304,12 @@ void GetEmbedderOptions(const FunctionCallbackInfo<Value>& args) {
.IsNothing())
return;

if (ret->Set(context,
FIXED_ONE_BYTE_STRING(env->isolate(), "hasEmbedderPreload"),
Boolean::New(isolate, env->embedder_preload() != nullptr))
.IsNothing())
return;

args.GetReturnValue().Set(ret);
}

Expand Down
9 changes: 9 additions & 0 deletions src/node_snapshotable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,13 @@ static void RunEmbedderEntryPoint(const FunctionCallbackInfo<Value>& args) {
}
}

static void RunEmbedderPreload(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(env->embedder_preload());
CHECK_EQ(args.Length(), 2);
env->embedder_preload()(env, args[0], args[1]);
}

void CompileSerializeMain(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsString());
Local<String> filename = args[0].As<String>();
Expand Down Expand Up @@ -1577,6 +1584,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,
Local<ObjectTemplate> target) {
Isolate* isolate = isolate_data->isolate();
SetMethod(isolate, target, "runEmbedderEntryPoint", RunEmbedderEntryPoint);
SetMethod(isolate, target, "runEmbedderPreload", RunEmbedderPreload);
SetMethod(isolate, target, "compileSerializeMain", CompileSerializeMain);
SetMethod(isolate, target, "setSerializeCallback", SetSerializeCallback);
SetMethod(isolate, target, "setDeserializeCallback", SetDeserializeCallback);
Expand All @@ -1590,6 +1598,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,

void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(RunEmbedderEntryPoint);
registry->Register(RunEmbedderPreload);
registry->Register(CompileSerializeMain);
registry->Register(SetSerializeCallback);
registry->Register(SetDeserializeCallback);
Expand Down
2 changes: 2 additions & 0 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Worker::Worker(Environment* env,
thread_id_(AllocateEnvironmentThreadId()),
name_(name),
env_vars_(env_vars),
embedder_preload_(env->embedder_preload()),
snapshot_data_(snapshot_data) {
Debug(this, "Creating new worker instance with thread id %llu",
thread_id_.id);
Expand Down Expand Up @@ -370,6 +371,7 @@ void Worker::Run() {
if (is_stopped()) return;
CHECK_NOT_NULL(env_);
env_->set_env_vars(std::move(env_vars_));
env_->set_embedder_preload(std::move(embedder_preload_));
SetProcessExitHandler(env_.get(), [this](Environment*, int exit_code) {
Exit(static_cast<ExitCode>(exit_code));
});
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class Worker : public AsyncWrap {

std::unique_ptr<MessagePortData> child_port_data_;
std::shared_ptr<KVStore> env_vars_;
EmbedderPreloadCallback embedder_preload_;

// A raw flag that is used by creator and worker threads to
// sync up on pre-mature termination of worker - while in the
Expand Down

0 comments on commit 66f52f5

Please sign in to comment.