From 260f152ecf14105acde66eb8929f7aedeaa4fb1e Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Mon, 6 Jul 2026 15:02:10 +0200 Subject: [PATCH] ffi: add getCurrentEventLoop Signed-off-by: Paolo Insogna Assisted-By: OpenAI:GPT-5.5 --- doc/api/ffi.md | 19 ++++++++++++++++++ lib/ffi.js | 2 ++ src/node_ffi.cc | 12 ++++++++++++ src/node_ffi.h | 1 + test/ffi/test-ffi-module.js | 33 ++++++++++++++++++++++++++++++++ test/ffi/test-ffi-permissions.js | 4 ++++ 6 files changed, 71 insertions(+) diff --git a/doc/api/ffi.md b/doc/api/ffi.md index 172af6bbc6edee..0623e876083b82 100644 --- a/doc/api/ffi.md +++ b/doc/api/ffi.md @@ -715,6 +715,25 @@ This is unsafe and dangerous. The returned pointer can become invalid if the underlying memory is detached, resized, transferred, or otherwise invalidated. Using stale pointers can cause memory corruption or process crashes. +## `ffi.getCurrentEventLoop()` + + + +* Returns: {bigint} + +Returns the address of the current thread's `uv_loop_t` as a `bigint`. + +The returned address is for the current Node.js environment. In the main thread, +this is the main thread event loop. In a worker thread, this is that worker's +event loop. + +This is unsafe and dangerous. The returned pointer is only valid for the lifetime +of the current environment. Using it after the environment exits, or from native +code that assumes a different thread or lifetime, can crash the process or +corrupt memory. + ## Safety notes The `node:ffi` module does not track pointer validity, memory ownership, or diff --git a/lib/ffi.js b/lib/ffi.js index 876e71334e3c2e..d91b6d4521e679 100644 --- a/lib/ffi.js +++ b/lib/ffi.js @@ -42,6 +42,7 @@ const { getUint64, getFloat32, getFloat64, + getCurrentEventLoop, exportBytes, getRawPointer, kFastArguments, @@ -320,6 +321,7 @@ module.exports = { getUint64, getFloat32, getFloat64, + getCurrentEventLoop, getRawPointer, setInt8, setUint8, diff --git a/src/node_ffi.cc b/src/node_ffi.cc index a5b51cb05c0692..8e5729cb4ee0d8 100644 --- a/src/node_ffi.cc +++ b/src/node_ffi.cc @@ -1215,6 +1215,17 @@ Local DynamicLibrary::GetConstructorTemplate( return tmpl; } +void GetCurrentEventLoop(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + THROW_IF_INSUFFICIENT_PERMISSIONS(env, permission::PermissionScope::kFFI, ""); + + args.GetReturnValue().Set(BigInt::NewFromUnsigned( + isolate, + static_cast(reinterpret_cast(env->event_loop())))); +} + // Module initialization. static void Initialize(Local target, Local unused, @@ -1230,6 +1241,7 @@ static void Initialize(Local target, SetMethod(context, target, "toArrayBuffer", ToArrayBuffer); SetMethod(context, target, "exportBytes", ExportBytes); SetMethod(context, target, "getRawPointer", GetRawPointer); + SetMethod(context, target, "getCurrentEventLoop", GetCurrentEventLoop); SetMethod(context, target, "getInt8", GetInt8); SetMethod(context, target, "getUint8", GetUint8); diff --git a/src/node_ffi.h b/src/node_ffi.h index dc44405121371e..a55cb74fc619e9 100644 --- a/src/node_ffi.h +++ b/src/node_ffi.h @@ -178,6 +178,7 @@ void ToBuffer(const v8::FunctionCallbackInfo& args); void ToArrayBuffer(const v8::FunctionCallbackInfo& args); void ExportBytes(const v8::FunctionCallbackInfo& args); void GetRawPointer(const v8::FunctionCallbackInfo& args); +void GetCurrentEventLoop(const v8::FunctionCallbackInfo& args); } // namespace node::ffi diff --git a/test/ffi/test-ffi-module.js b/test/ffi/test-ffi-module.js index 573c5e7ce5947b..ecbd6f1c6a9818 100644 --- a/test/ffi/test-ffi-module.js +++ b/test/ffi/test-ffi-module.js @@ -5,6 +5,7 @@ const { spawnSyncAndAssert } = require('../common/child_process'); const assert = require('node:assert'); const { spawnSync } = require('node:child_process'); const { test } = require('node:test'); +const { Worker } = require('node:worker_threads'); common.skipIfFFIMissing(); @@ -87,6 +88,7 @@ test('ffi exports expected API surface', () => { 'exportArrayBufferView', 'exportBuffer', 'exportString', + 'getCurrentEventLoop', 'getFloat32', 'getFloat64', 'getInt16', @@ -135,6 +137,7 @@ test('ffi exports expected API surface', () => { assert.strictEqual(typeof ffi.getUint64, 'function'); assert.strictEqual(typeof ffi.getFloat32, 'function'); assert.strictEqual(typeof ffi.getFloat64, 'function'); + assert.strictEqual(typeof ffi.getCurrentEventLoop, 'function'); assert.strictEqual(typeof ffi.setInt8, 'function'); assert.strictEqual(typeof ffi.setUint8, 'function'); assert.strictEqual(typeof ffi.setInt16, 'function'); @@ -180,3 +183,33 @@ test('ffi.types exports canonical type constants', () => { assert.deepStrictEqual(ffi.types, expected); assert.strictEqual(Object.isFrozen(ffi.types), true); }); + +test('ffi.getCurrentEventLoop returns the current thread event loop address', async () => { + const ffi = require('node:ffi'); + const mainLoop = ffi.getCurrentEventLoop(); + + assert.strictEqual(typeof mainLoop, 'bigint'); + assert.ok(mainLoop > 0n); + assert.strictEqual(ffi.getCurrentEventLoop(), mainLoop); + + const workerLoop = await new Promise((resolve, reject) => { + const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + const ffi = require('node:ffi'); + const loop = ffi.getCurrentEventLoop(); + + parentPort.postMessage([loop, ffi.getCurrentEventLoop()]); + `, { eval: true }); + + worker.on('message', resolve); + worker.on('error', reject); + worker.on('exit', (code) => { + if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); + }); + }); + + assert.strictEqual(typeof workerLoop[0], 'bigint'); + assert.ok(workerLoop[0] > 0n); + assert.strictEqual(workerLoop[0], workerLoop[1]); + assert.notStrictEqual(workerLoop[0], mainLoop); +}); diff --git a/test/ffi/test-ffi-permissions.js b/test/ffi/test-ffi-permissions.js index c07f0bbdb439d7..970f5f019ee7b6 100644 --- a/test/ffi/test-ffi-permissions.js +++ b/test/ffi/test-ffi-permissions.js @@ -70,6 +70,10 @@ test('permission model blocks ffi memory and helper APIs', () => { ffi.getRawPointer(Buffer.alloc(0)); }, denied); + assert.throws(() => { + ffi.getCurrentEventLoop(); + }, denied); + assert.throws(() => { ffi.dlclose({ close() {} }); }, denied);