From 883bf77a94d8969a3aea9839956afab16f8e846f Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 15 Jun 2026 11:42:19 -0400 Subject: [PATCH] child_process: pass spawn options to the binding positionally ChildProcess.prototype.spawn() handed a single options object to the ProcessWrap::Spawn() binding, which then read about a dozen properties back out individually with Object::Get(). Each of those is a property lookup across the JS/C++ boundary on every spawn. Pass file, args, cwd, envPairs, stdio, uid and gid as positional arguments and pack the boolean flags (detached, windowsHide, windowsVerbatimArguments) into a single integer whose bit values are exported from the binding as `constants`. The native side then reads each value directly from the call arguments. Add internal typings for the process_wrap binding (previously untyped) describing the new positional spawn() signature and the exported constants. There is no observable behavior change. Spawn wall-clock time is dominated by the operating system process-creation cost and is unchanged; this reduces the per-spawn work done on the main thread and clarifies the contract between the JS and C++ layers. Signed-off-by: Yagiz Nizipli --- lib/internal/child_process.js | 21 +++- src/process_wrap.cc | 112 ++++++++-------------- typings/globals.d.ts | 2 + typings/internalBinding/process_wrap.d.ts | 43 +++++++++ 4 files changed, 102 insertions(+), 76 deletions(-) create mode 100644 typings/internalBinding/process_wrap.d.ts diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index 9eac06d1fdf145..a12b2954db81de 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -41,7 +41,7 @@ const dgram = require('dgram'); const inspect = require('internal/util/inspect').inspect; const assert = require('internal/assert'); -const { Process } = internalBinding('process_wrap'); +const { Process, constants: processConstants } = internalBinding('process_wrap'); const { WriteWrap, kReadBytesOrError, @@ -397,7 +397,24 @@ ChildProcess.prototype.spawn = function spawn(options) { childProcessSpawn.start.publish({ process: this, options }); } - const err = this._handle.spawn(options); + let spawnFlags = 0; + if (options.detached) + spawnFlags |= processConstants.kProcessFlagDetached; + if (options.windowsHide) + spawnFlags |= processConstants.kProcessFlagWindowsHide; + if (options.windowsVerbatimArguments) + spawnFlags |= processConstants.kProcessFlagWindowsVerbatimArguments; + + const err = this._handle.spawn( + options.file, + options.args, + options.cwd, + options.envPairs, + options.stdio, + spawnFlags, + options.uid, + options.gid, + ); // Run-time errors should emit an error, not throw an exception. if (err === UV_EACCES || diff --git a/src/process_wrap.cc b/src/process_wrap.cc index d27ca7da7b587b..21ccb2a9989b9d 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -49,10 +49,18 @@ using v8::Nothing; using v8::Number; using v8::Object; using v8::String; +using v8::Uint32; using v8::Value; namespace { +enum ProcessFlags : uint32_t { + kProcessFlagNone = 0, + kProcessFlagDetached = 1 << 0, + kProcessFlagWindowsHide = 1 << 1, + kProcessFlagWindowsVerbatimArguments = 1 << 2, +}; + class ProcessWrap : public HandleWrap { public: static void Initialize(Local target, @@ -71,6 +79,12 @@ class ProcessWrap : public HandleWrap { SetProtoMethod(isolate, constructor, "kill", Kill); SetConstructorFunction(context, target, "Process", constructor); + + Local constants = Object::New(isolate); + NODE_DEFINE_CONSTANT(constants, kProcessFlagDetached); + NODE_DEFINE_CONSTANT(constants, kProcessFlagWindowsHide); + NODE_DEFINE_CONSTANT(constants, kProcessFlagWindowsVerbatimArguments); + target->Set(context, env->constants_string(), constants).Check(); } static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -118,14 +132,9 @@ class ProcessWrap : public HandleWrap { static Maybe ParseStdioOptions( Environment* env, - Local js_options, + Local stdios_val, std::vector* options_stdio) { Local context = env->context(); - Local stdio_key = env->stdio_string(); - Local stdios_val; - if (!js_options->Get(context, stdio_key).ToLocal(&stdios_val)) { - return Nothing(); - } if (!stdios_val->IsArray()) { THROW_ERR_INVALID_ARG_TYPE(env, "options.stdio must be an array"); return Nothing(); @@ -188,34 +197,21 @@ class ProcessWrap : public HandleWrap { ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This()); int err = 0; - if (!args[0]->IsObject()) { - return THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); - } - - Local js_options = args[0].As(); - uv_process_options_t options; memset(&options, 0, sizeof(uv_process_options_t)); options.exit_cb = OnExit; - // options.file - Local file_v; - if (!js_options->Get(context, env->file_string()).ToLocal(&file_v)) { - return; - } - CHECK(file_v->IsString()); - node::Utf8Value file(env->isolate(), file_v); + // args[0] file + CHECK(args[0]->IsString()); + node::Utf8Value file(env->isolate(), args[0]); options.file = *file; THROW_IF_INSUFFICIENT_PERMISSIONS( env, permission::PermissionScope::kChildProcess, file.ToStringView()); - // options.uid - Local uid_v; - if (!js_options->Get(context, env->uid_string()).ToLocal(&uid_v)) { - return; - } + // args[6] uid + Local uid_v = args[6]; if (!uid_v->IsUndefined() && !uid_v->IsNull()) { CHECK(uid_v->IsInt32()); const int32_t uid = uid_v.As()->Value(); @@ -223,11 +219,8 @@ class ProcessWrap : public HandleWrap { options.uid = static_cast(uid); } - // options.gid - Local gid_v; - if (!js_options->Get(context, env->gid_string()).ToLocal(&gid_v)) { - return; - } + // args[7] gid + Local gid_v = args[7]; if (!gid_v->IsUndefined() && !gid_v->IsNull()) { CHECK(gid_v->IsInt32()); const int32_t gid = gid_v.As()->Value(); @@ -244,15 +237,11 @@ class ProcessWrap : public HandleWrap { err = UV_EINVAL; #endif - // options.args - Local argv_v; - if (!js_options->Get(context, env->args_string()).ToLocal(&argv_v)) { - return; - } + // args[1] args std::vector options_args; std::vector args_vals; - if (argv_v->IsArray()) { - Local js_argv = argv_v.As(); + if (args[1]->IsArray()) { + Local js_argv = args[1].As(); int argc = js_argv->Length(); CHECK_LT(argc, INT_MAX); // Check for overflow. args_vals.reserve(argc); @@ -273,26 +262,18 @@ class ProcessWrap : public HandleWrap { options.args = options_args.data(); } - // options.cwd - Local cwd_v; - if (!js_options->Get(context, env->cwd_string()).ToLocal(&cwd_v)) { - return; - } + // args[2] cwd node::Utf8Value cwd(env->isolate(), - cwd_v->IsString() ? cwd_v : Local()); + args[2]->IsString() ? args[2] : Local()); if (cwd.length() > 0) { options.cwd = *cwd; } - // options.env - Local env_v; - if (!js_options->Get(context, env->env_pairs_string()).ToLocal(&env_v)) { - return; - } + // args[3] envPairs std::vector options_env; std::vector env_vals; - if (env_v->IsArray()) { - Local env_opt = env_v.As(); + if (args[3]->IsArray()) { + Local env_opt = args[3].As(); int envc = env_opt->Length(); CHECK_LT(envc, INT_MAX); // Check for overflow. env_vals.reserve(envc); @@ -313,22 +294,19 @@ class ProcessWrap : public HandleWrap { options.env = options_env.data(); } - // options.stdio + // args[4] stdio std::vector options_stdio; - if (ParseStdioOptions(env, js_options, &options_stdio).IsNothing()) { + if (ParseStdioOptions(env, args[4], &options_stdio).IsNothing()) { return; } options.stdio = options_stdio.data(); options.stdio_count = options_stdio.size(); - // options.windowsHide - Local hide_v; - if (!js_options->Get(context, env->windows_hide_string()) - .ToLocal(&hide_v)) { - return; - } + // args[5] flags (detached, windowsHide, windowsVerbatimArguments) + CHECK(args[5]->IsUint32()); + const uint32_t flags = args[5].As()->Value(); - if (hide_v->IsTrue()) { + if (flags & kProcessFlagWindowsHide) { options.flags |= UV_PROCESS_WINDOWS_HIDE; } @@ -336,25 +314,11 @@ class ProcessWrap : public HandleWrap { options.flags |= UV_PROCESS_WINDOWS_HIDE_CONSOLE; } - // options.windows_verbatim_arguments - Local wva_v; - if (!js_options->Get(context, env->windows_verbatim_arguments_string()) - .ToLocal(&wva_v)) { - return; - } - - if (wva_v->IsTrue()) { + if (flags & kProcessFlagWindowsVerbatimArguments) { options.flags |= UV_PROCESS_WINDOWS_VERBATIM_ARGUMENTS; } - // options.detached - Local detached_v; - if (!js_options->Get(context, env->detached_string()) - .ToLocal(&detached_v)) { - return; - } - - if (detached_v->IsTrue()) { + if (flags & kProcessFlagDetached) { options.flags |= UV_PROCESS_DETACHED; } diff --git a/typings/globals.d.ts b/typings/globals.d.ts index ddd5885faa057f..b8dd8bbc9f7f5e 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -17,6 +17,7 @@ import { MessagingBinding } from './internalBinding/messaging'; import { OptionsBinding } from './internalBinding/options'; import { OSBinding } from './internalBinding/os'; import { ProcessBinding } from './internalBinding/process'; +import { ProcessWrapBinding } from './internalBinding/process_wrap'; import { SeaBinding } from './internalBinding/sea'; import { SerdesBinding } from './internalBinding/serdes'; import { StringDecoderBinding } from './internalBinding/string_decoder'; @@ -53,6 +54,7 @@ interface InternalBindingMap { options: OptionsBinding; os: OSBinding; process: ProcessBinding; + process_wrap: ProcessWrapBinding; sea: SeaBinding; serdes: SerdesBinding; string_decoder: StringDecoderBinding; diff --git a/typings/internalBinding/process_wrap.d.ts b/typings/internalBinding/process_wrap.d.ts new file mode 100644 index 00000000000000..8e5ab490ef9778 --- /dev/null +++ b/typings/internalBinding/process_wrap.d.ts @@ -0,0 +1,43 @@ +import { owner_symbol } from './symbols'; + +declare namespace InternalProcessWrapBinding { + type StdioType = 'ignore' | 'pipe' | 'overlapped' | 'wrap' | 'inherit' | 'fd'; + + interface StdioContainer { + type: StdioType; + handle?: object; + fd?: number; + } + + class Process { + constructor(); + [owner_symbol]?: object; + pid: number; + onexit: (exitCode: number, signalCode: string) => void; + spawn( + file: string, + args: string[] | undefined, + cwd: string | undefined, + envPairs: string[] | undefined, + stdio: StdioContainer[], + flags: number, + uid: number | null | undefined, + gid: number | null | undefined, + ): number; + kill(signal: number): number; + ref(): void; + unref(): void; + close(callback?: () => void): void; + } + + interface ProcessConstants { + kProcessFlagDetached: number; + kProcessFlagWindowsHide: number; + kProcessFlagWindowsVerbatimArguments: number; + } +} + +export interface ProcessWrapBinding { + Process: typeof InternalProcessWrapBinding.Process; + constants: InternalProcessWrapBinding.ProcessConstants; +}