Skip to content

Latest commit

 

History

History
904 lines (677 loc) · 29.9 KB

File metadata and controls

904 lines (677 loc) · 29.9 KB

← Getting Started · Back to README · INodeBridge API →

API Reference

Complete reference for COM interfaces, configuration structures, and error codes.

Usage pattern:

DN_CreateFactory → INodeFactory
  └─► CreateRuntime → INodeRuntime
        ├─► as INodeLog     (QI: BE136A3D-...)
        ├─► as INodeNpm     (QI: A4AEE322-...)
        └─► as INodeBridge  (QI: E5F6A7B8-...)

Contents

  1. Flat C Exports
  2. INodeFactory
  3. INodeRuntime
  4. INodeLog
  5. INodeNpm
  6. INodeNpmRunner
  7. INodeBridge
  8. Configuration Structures
  9. Constants and HRESULT Codes
  10. Release Order
  11. Thread-affinity reference

Flat C Exports

The only stable flat exports of the DLL. Everything else is accessed via vtable.

DN_CreateFactory

function DN_CreateFactory(
  pConfig   : PDNFactoryConfig;   // nil → defaults
  out ppFactory: INodeFactory     // refcount = 1
): HRESULT; cdecl;

Creates (or returns the existing) process-wide Node.js factory. One factory per process.

Call once at application startup. A second call without releasing the previous factory returns DN_E_INVALID_STATE.

Result Meaning
S_OK Factory created successfully
E_POINTER ppFactory = nil
E_INVALIDARG Invalid configuration
E_OUTOFMEMORY Out of memory
DN_E_INVALID_STATE Factory already exists

DN_PathToFileUrl

function DN_PathToFileUrl(
  pathUtf8  : PAnsiChar;   // Windows path, UTF-8
  bufUtf8   : PAnsiChar;   // output buffer (nil → query length only)
  cap       : Cardinal;    // buffer capacity
  pWritten  : PCardinal    // bytes written (may be nil)
): HRESULT; cdecl;

Converts a Windows path to a canonical file: URL (WHATWG/Node.js semantics).

Required for virtual_filename_utf8 with ESM scripts: Node.js requires a file: URL as resource_name for ESM.

var
  Buf: array[0..511] of AnsiChar;
begin
  DN_PathToFileUrl('C:\scripts\main.mjs', @Buf, SizeOf(Buf), nil);
  // Buf → 'file:///C:/scripts/main.mjs'
end;

INodeFactory

GUID: {790CB9D8-0989-49B0-A1DE-1AC00DF9BDA6} (v2: ShutdownPlatform added)

Manages the process-wide Node.js/V8 state. Created by DN_CreateFactory.

Supports QueryInterface for INodeLog (factory-level log).

CreateRuntime

function CreateRuntime(
  pConfig   : PDNRuntimeConfig;    // config (copied — may be freed after the call)
  out ppRuntime: INodeRuntime      // refcount = 1
): HRESULT; stdcall;

Creates a new runtime instance. V1 supports one active runtime per process.

GetLastError

function GetLastError(
  bufUtf8  : PAnsiChar;   // nil → query length
  cap      : Cardinal;
  pWritten : PCardinal
): HRESULT; stdcall;

Returns the extended error message for the last failed call (thread-local).

GetVersion

function GetVersion(
  bufUtf8 : PAnsiChar;
  cap     : Cardinal
): HRESULT; stdcall;

Returns a version string in the format delphiLibNodeJS/1.0.0 node/26.x.x.

GetNpmRunner

function GetNpmRunner(out ppRunner: INodeNpmRunner): HRESULT; stdcall;

Returns (or lazily creates) the singleton embedded npm runner for this factory. npm output is available via QueryInterface(IID_INodeLog) on the returned object.

ShutdownPlatform

function ShutdownPlatform: HRESULT; stdcall;

Deterministic shutdown of the V8 platform. This is a process-wide operation — must be the last call to the DLL before FreeLibrary / process exit.

Preconditions:

  • All INodeRuntime instances must be stopped and released (Shutdown + Release).
  • After this call, a new DN_CreateFactory will return DN_E_INVALID_STATE.

Usage pattern:

Runtime.Shutdown(5000);
Runtime.Release;
Factory.ShutdownPlatform;   // 4. platform shut down
Factory.Release;
// now safe to FreeLibrary / process exit
Result Meaning
S_OK Platform shut down (idempotent)
DN_E_INVALID_STATE Active runtimes exist; stop them first

INodeRuntime

GUID: {7C8D9E0F-1A2B-3C4D-5E6F-7A8B9C0D1E2F}

Manages the lifecycle of a single Node.js event loop.

Recommended lifecycle: CreateRuntimeStart(work)Shutdown(Release)

Classic pattern: StartStopJoin(Release)

Supports QueryInterface for INodeLog, INodeNpm, INodeBridge.

State constants (DN_RS_*)

Constant Value Description
DN_RS_CREATED 0 Created, Start not yet called
DN_RS_RUNNING 1 Event loop running
DN_RS_STOPPING 2 Stop called, waiting for event loop
DN_RS_STOPPED 3 Worker thread finished, V8 released

Start

function Start: HRESULT; stdcall;

Starts the event loop on a separate OS thread. Non-blocking — returns immediately after the thread starts.

Result Meaning
S_OK Thread started
DN_E_INVALID_STATE Start has already been called

Stop

function Stop: HRESULT; stdcall;

Requests graceful shutdown (sends process.exit to the event loop). Non-blocking. Thread-safe.

Join

function Join(
  timeoutMs : Cardinal;    // DN_TIMEOUT_INFINITE ($FFFFFFFF) = wait forever; 0 = return immediately
  pExitCode : PInteger     // Node.js exit code (may be nil)
): HRESULT; stdcall;

Waits for the event loop thread to finish.

Result Meaning
S_OK Clean exit
DN_E_SCRIPT_EXCEPTION Entry script threw an unhandled exception

RunScriptFile

function RunScriptFile(
  pathUtf8 : PAnsiChar    // absolute path or path relative to scripts_path
): HRESULT; stdcall;

Loads and executes a JS file. The format (CJS/ESM) is determined automatically by extension (.mjs → ESM, .cjs → CJS, .js → per configuration).

RunScriptText

function RunScriptText(
  sourceUtf8           : PAnsiChar;   // JavaScript source text
  virtualFilenameUtf8  : PAnsiChar    // virtual filename (nil = auto); for ESM use a file: URL
): HRESULT; stdcall;

Executes inline JavaScript. Can be called multiple times after Start to inject commands.

// Simple call
Runtime.RunScriptText('console.log("hello")', nil);

// For ESM, virtualFilename must be a file: URL
var
  VFileUrl: array[0..511] of AnsiChar;
begin
  DN_PathToFileUrl(PAnsiChar(AnsiString(TmpDir + '\main.mjs')),
                   @VFileUrl, SizeOf(VFileUrl), nil);
  Runtime.RunScriptText(PAnsiChar(EsmSource), @VFileUrl);
end;

Shutdown (preferred shutdown method)

function Shutdown(
  timeoutMs : Cardinal    // DN_TIMEOUT_INFINITE ($FFFFFFFF) = wait forever; 0 = poll once
): HRESULT; stdcall;

Atomic Stop + wait for the worker thread. All V8 cleanup runs inside the worker thread under v8::Locker, preventing ACCESS_VIOLATION on runtime destruction. Once Shutdown returns, the object can be released immediately.

Result Meaning
S_OK Completed cleanly
S_FALSE Already stopped
DN_E_INVALID_STATE Start has not been called
HRESULT_FROM_WIN32(ERROR_TIMEOUT) timeoutMs exceeded
// Recommended shutdown pattern
NodeCheck(Runtime.Shutdown(DN_TIMEOUT_INFINITE), Factory);  // wait forever
Runtime := nil;                                             // Release

GetState

function GetState(
  out state : Cardinal    // receives a DN_RS_* constant
): HRESULT; stdcall;

Returns the current runtime state. Analogous to TComponent.ComponentState in the Delphi RTL.

var State: Cardinal;
Runtime.GetState(State);
WriteLn('State: ', RuntimeStateName(State));  // 'RUNNING', 'STOPPED', ...

EnableInspector

function EnableInspector(
  hostUtf8     : PAnsiChar;   // listening host, e.g. '127.0.0.1'; nil = '127.0.0.1'
  port         : Word;        // DevTools port, typically 9229
  pauseOnStart : LongBool     // True = wait for debugger connection before executing code
): HRESULT; stdcall;

Enables the V8 Inspector (Chrome DevTools Protocol). Called before Start. The debugger connects in Chrome/Edge at devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=<host>:<port>.

Result Meaning
S_OK Inspector scheduled; activates on Start
DN_E_INVALID_STATE Runtime is already running
E_INVALIDARG Invalid parameters; or hostUtf8 is not a loopback address (127.0.0.1, ::1, localhost) — non-loopback is forbidden by default
// Enable Inspector on port 9229, wait for debugger
NodeCheck(Runtime.EnableInspector('127.0.0.1', 9229, True), Factory);
NodeCheck(Runtime.Start, Factory);
// Open chrome://inspect in Chrome → Remote Target → inspect

WaitReady

function WaitReady(
  timeoutMs   : Cardinal;   // max wait in ms; DN_TIMEOUT_INFINITE ($FFFFFFFF) = wait forever
  pStartupHr  : PHRESULT    // optional; receives startup result code
): HRESULT; stdcall;

Blocks the calling thread until the runtime has finished starting up and is ready to accept CallFunction/PostFunction calls.

Readiness semantics:

  • CJS / synchronous scripts: signals immediately after LoadEnvironment returns — the synchronous frame has completed.
  • ESM modules with top-level await: signals only after the entry-module Promise settles (fulfills or rejects). This means WaitReady is safe to use as a "fully initialized" signal — all top-level await chains have resolved before it returns S_OK.

Call after Start() to reliably determine whether the runtime is ready for CallFunction.

Result Meaning
S_OK Ready event received; check *pStartupHr for startup outcome
HRESULT_FROM_WIN32(ERROR_TIMEOUT) Timeout elapsed before startup completed
DN_E_INVALID_STATE Start() has not been called yet

*pStartupHr receives one of:

  • S_OK — entry script / ESM module initialized successfully
  • DN_E_SCRIPT_EXCEPTION — entry script threw a top-level exception, or ESM await rejected
  • DN_E_RUNTIME_STOPPED — runtime was stopped before the ESM Promise settled
  • E_FAIL — internal bootstrap error
NodeCheck(Runtime.Start, Factory);
var startupHr: HRESULT;
var hr := Runtime.WaitReady(5000, @startupHr);
if hr = HRESULT_FROM_WIN32(ERROR_TIMEOUT) then
  raise ENodeError.Create('Runtime failed to start within 5 seconds')
else if FAILED(startupHr) then
  raise ENodeError.CreateFmt('Entry script error: %s', [Runtime.GetLastError]);

INodeLog

GUID: {BE136A3D-0D3A-45F8-B928-B19A96B948A6}

Provides access to the log ring buffer. Obtained via QI on INodeFactory or INodeRuntime.

Log := Runtime as INodeLog;      // or: Factory as INodeLog

Captures: console.log/warn/error, process.stdout/stderr.write, unhandled exceptions, process.on('warning').

SetCallback — push mode

function SetCallback(
  pfn       : TDNLogCallback;   // nil → remove callback
  pUserData : Pointer
): HRESULT; stdcall;

Registers a callback invoked on the event loop thread. Must be non-blocking. Exceptions must not cross the ABI boundary — wrap in try..except.

procedure MyLogCallback(pRecord: PDNLogRecord; pUserData: Pointer); cdecl;
begin
  try
    Writeln(string(pRecord^.message_utf8));
  except
    // always catch — exception across ABI = UB
  end;
end;

// Register before Start:
Log.SetCallback(@MyLogCallback, nil);

SetLevel

function SetLevel(Level: Cardinal): HRESULT; stdcall;

Sets the minimum log level. Records below the level are discarded. Default: DN_LOG_DEBUG.

Poll — pull mode

function Poll(pOut: PDNLogRecord): HRESULT; stdcall;

Extracts one record from the ring buffer.

Result Meaning
S_OK Record retrieved
S_FALSE Buffer empty

Important: pOut.source_utf8 and message_utf8 are valid only until the next Poll call.

// Typical polling loop
var Rec: TDNLogRecord;
while Log.Poll(@Rec) = S_OK do
begin
  case Rec.level of
    DN_LOG_INFO : Writeln('[I] ' + string(Rec.message_utf8));
    DN_LOG_WARN : Writeln('[W] ' + string(Rec.message_utf8));
    DN_LOG_ERROR: Writeln('[E] ' + string(Rec.message_utf8));
  end;
end;

INodeNpm

GUID: {A4AEE322-C1B0-4E4D-880E-B4FBC75BF0AD}

Runs npm operations from Delphi. Obtained via QI on INodeRuntime.

Npm := Runtime as INodeNpm;

npm is launched via CreateProcess("cmd.exe /C npm …"). Requires npm.cmd on %PATH%.

Exec

function Exec(
  pCfg     : PDNNpmConfig;   // nil → defaults
  argvUtf8 : PAnsiChar       // space-separated arguments, e.g. 'run build'
): HRESULT; stdcall;

Executes an arbitrary npm command synchronously.

Install

function Install(
  pCfg        : PDNNpmConfig;
  packageUtf8 : PAnsiChar     // package name, e.g. 'lodash@4'
): HRESULT; stdcall;

Runs npm install <package> in pCfg.work_dir_utf8.

InstallFromPackageJson

function InstallFromPackageJson(
  pCfg : PDNNpmConfig    // work_dir_utf8 = directory containing package.json
): HRESULT; stdcall;

Runs npm install (reads package.json) in pCfg.work_dir_utf8.

var
  NpmCfg: TDNNpmConfig;
  WorkDir: AnsiString;
begin
  WorkDir := AnsiString(CommonDir);
  FillChar(NpmCfg, SizeOf(NpmCfg), 0);
  NpmCfg.size := SizeOf(NpmCfg);
  NpmCfg.work_dir_utf8 := PAnsiChar(WorkDir);
  OleCheck(Npm.InstallFromPackageJson(@NpmCfg));
end;

INodeNpmRunner

GUID: {848A16FC-57BE-4EF3-A5CB-3D96EBFAC23B}

Embedded npm without a system installation. Obtained via INodeFactory.GetNpmRunner() or QI(IID_INodeNpmRunner) on the factory.

var Runner: INodeNpmRunner;
Runner := Factory as INodeNpmRunner;  // or Factory.GetNpmRunner(Runner)

Operates in two modes (selected automatically):

  • Mode C-1 — uses npm-cli.js from <dll-dir>\npm\bin\npm-cli.js (files on disk).
  • Mode C-2 — uses an esbuild bundle embedded directly in the DLL as const char[] (no files on disk required).

Structure DN_NPM_RUNNER_CONFIG

type
  TDNNpmRunnerConfig = record
    size          : Cardinal;     // SizeOf(TDNNpmRunnerConfig) — versioning guard
    work_dir_utf8 : PAnsiChar;    // working directory for npm (--prefix); required
    node_exe_utf8 : PAnsiChar;    // path to node.exe for lifecycle scripts; nil = auto-detect
    flags         : Cardinal;     // combination of DN_NPM_RUNNER_* flags
    join_timeout_ms: Cardinal;    // wait timeout; 0 = 10 minutes
  end;

flags constants:

Constant Value Description
DN_NPM_RUNNER_ENABLE_SCRIPTS $0001 Allow lifecycle scripts (preinstall/postinstall)
DN_NPM_RUNNER_FORCE_BUNDLE $0002 Force Mode C-2 (bundle)
DN_NPM_RUNNER_FORCE_DISK $0004 Force Mode C-1 (files on disk)

RunNpm

function RunNpm(
  pConfig       : PDNNpmRunnerConfig;  // configuration; required; work_dir_utf8 is required
  commandUtf8   : PAnsiChar;           // npm command, e.g. 'install lodash@4 --save'
  pExitCode     : PInteger             // receives npm exit code; 0 = success; nil = ignore
): HRESULT; stdcall;
Result Meaning
S_OK npm finished (check *pExitCode)
E_INVALIDARG pConfig or commandUtf8 = nil / invalid size
E_NOTIMPL Neither Mode C-1 files nor Mode C-2 bundle is available
E_FAIL Internal runtime launch error
var
  Runner    : INodeNpmRunner;
  RunCfg    : TDNNpmRunnerConfig;
  ExitCode  : Integer;
  WorkDir   : UTF8String;
begin
  Runner  := Factory as INodeNpmRunner;
  WorkDir := UTF8String(GetCurrentDir);
  FillChar(RunCfg, SizeOf(RunCfg), 0);
  RunCfg.size          := SizeOf(RunCfg);
  RunCfg.work_dir_utf8 := PAnsiChar(WorkDir);
  NodeCheck(Runner.RunNpm(@RunCfg, 'install lodash@4 --save', @ExitCode), Factory);
  if ExitCode <> 0 then
    raise Exception.CreateFmt('npm exited with code %d', [ExitCode]);
end;

npm output is available via QI(IID_INodeLog) on the INodeNpmRunner object. npm stdout/stderr is captured and placed in the ring log.


INodeBridge

GUID: {E5F6A7B8-C9D0-4E1F-2A3B-4C5D6E7F8A9B} (v7: AllocSharedBuffer + IDNSharedBufferHandle)

Bidirectional Delphi↔JS bridge. Obtained via QI on INodeRuntime after Start.

Bridge := Runtime as INodeBridge;

Direction A (Delphi→JS): CallFunction — calls a globalThis function with JSON arguments; blocks the calling thread until a JSON result is received.

Direction B (JS→Delphi): RegisterExport — publishes a Delphi object (IDispatch) as globalThis.bridge.<ns> in JS.

Lifecycle: after Runtime.Shutdown() / Runtime.Stop()+Join(), the bridge automatically detaches from V8 (via DetachFromV8()). Subsequent CallFunction calls immediately return DN_E_INVALID_STATE — no Access Violation. Releasing Bridge := nil is safe at any point after Shutdown.

CallFunction

function CallFunction(
  fnNameUtf8    : PAnsiChar;     // function name in globalThis
  argsJsonUtf8  : PAnsiChar;     // JSON argument array, e.g. '[1, "hello"]'
  timeoutMs     : Cardinal;      // DN_TIMEOUT_INFINITE ($FFFFFFFF) = wait forever
  pResult       : PDNCallResult  // receives result (nil = ignore)
): HRESULT; stdcall;

Calls globalThis.<fnName>(args...) in the JS environment, blocking the calling thread.

Result Meaning
S_OK Success; pResult->json_utf8 contains the JSON response
DN_E_NOT_REGISTERED Function not found on globalThis
DN_E_CALL_FAILED JS threw an exception or returned a rejection
DN_E_CALL_TIMEOUT timeoutMs expired
DN_E_INVALID_STATE Runtime is not running or has been stopped
var Res: TDNCallResult;
NodeCheck(Bridge.CallFunction('calcTax', '[1234.56, 0.2]', 5000, @Res), Factory);
WriteLn(string(AnsiString(Res.json_utf8)));  // "246.912"
Bridge.FreeResult(@Res);

FreeResult

function FreeResult(pResult: PDNCallResult): HRESULT; stdcall;

Frees the string allocated by the DLL. Passing nil or pResult->json_utf8 = nil is a no-op.

RegisterExport

function RegisterExport(
  nsUtf8 : PAnsiChar;  // namespace name (JS identifier)
  pDisp  : IDispatch;  // Delphi object (AddRef'd by the bridge)
  flags  : Cardinal;   // DN_EXPORT_WORKER or DN_EXPORT_MAIN
  hwnd   : HWND        // only for DN_EXPORT_MAIN; 0 for WORKER
): HRESULT; stdcall;

Publishes a Delphi object as globalThis.bridge.<ns> in JS. Methods with the Async suffix in JS return a Promise.

Flag Meaning
DN_EXPORT_WORKER (0) IDispatch::Invoke is called directly on the event loop thread
DN_EXPORT_MAIN (1) Call is marshaled via WM_DN_DISPATCH to the Delphi thread (hwnd); JS receives a Promise
// Publish object for Direction B
var Disp: IDispatch;
Disp := TDNExport.Wrap(TMyService.Create);
Bridge.RegisterExport('svc', Disp, DN_EXPORT_WORKER, 0);
// In JS: const v = bridge.svc.GetVersion();

ProcessMainThreadPending

function ProcessMainThreadPending: HRESULT; stdcall;

For DN_EXPORT_MAIN: call from WndProc when WM_DN_DISPATCH is received. Executes all pending IDispatch::Invoke on the current thread and signals JS completion.

Use TDNMainDispatcher from NodeBridgeHelpers.pas — it creates a hidden window and handles WM_DN_DISPATCH automatically.

Helper classes (NodeBridgeHelpers.pas)

Class Purpose
TDNCallProxy Typed wrapper for CallFunction — accepts/returns AnsiString
TDNExport.Wrap Publishes a TObject with published methods as IDispatch for RegisterExport
TDNMainDispatcher Creates a hidden window, handles WM_DN_DISPATCH on the main thread
BridgeCallFunction Procedural wrapper for a single call without manual Proxy management
// Direction A — blocking call via TDNCallProxy
var Proxy: TDNCallProxy;
Proxy := TDNCallProxy.Create(Bridge, 30000 {ms});
try
  ResultJson := Proxy.Call('loadAndRender', ArgsJson);
finally
  Proxy.Free;
end;

Configuration Structures

Always: FillChar(Cfg, SizeOf(Cfg), 0) + Cfg.size := SizeOf(Cfg) before use.

TDNFactoryConfig

Passed to DN_CreateFactory. All fields are optional (nil/0 = defaults).

Field Type Description
size Cardinal Required: SizeOf(TDNFactoryConfig)
argv0_utf8 PAnsiChar process.argv[0]; nil → 'node'
process_exec_path_utf8 PAnsiChar process.execPath; nil → argv0_utf8
initial_node_options_utf8 PAnsiChar Additional NODE_OPTIONS; nil = none
platform_thread_pool_size Cardinal V8 platform threads; 0 = default
libuv_threadpool_size Cardinal UV_THREADPOOL_SIZE; 0 = 4
max_js_worker_threads Cardinal worker_threads limit; 0 = default
log_capacity Cardinal Ring buffer slots; 0 = 4096
flags Cardinal Reserved; 0

TDNRuntimeConfig

Passed to INodeFactory.CreateRuntime.

Field Type Description
size Cardinal Required: SizeOf(TDNRuntimeConfig)
scripts_path_utf8 PAnsiChar Base directory / cwd; nil → exe directory
script_path_utf8 PAnsiChar Path to the entry file .js/.mjs/.cjs; nil if script_text_utf8 is used
script_text_utf8 PAnsiChar Inline JS source; nil if script_path_utf8 is used
virtual_filename_utf8 PAnsiChar Virtual filename for script_text; for ESM — a file: URL
node_options_utf8 PAnsiChar Per-runtime NODE_OPTIONS
env_block_utf8 PAnsiChar Double-NUL-terminated block of KEY=VALUE\0KEY=VALUE\0\0; nil = inherit parent env
extra_module_paths_utf8 PAnsiChar Additional paths in NODE_PATH
module_format Cardinal DN_MODULE_FORMAT_*
flags Cardinal DN_RUNTIME_ALLOW_*

extra_module_paths_utf8 — path to the node_modules directory (not its parent):

CommonModules := AnsiString(CommonDir + '\node_modules');
Cfg.extra_module_paths_utf8 := PAnsiChar(CommonModules);
// CommonModules must remain alive until CreateRuntime is called!

Known Limitation — process-wide NODE_PATH

extra_module_paths_utf8 is implemented by setting the process-wide NODE_PATH environment variable for the duration of the runtime. If two TNodeRuntime instances with different extra_module_paths are started concurrently, the second Start() will capture the already-modified NODE_PATH as its "original" value, and restoring it on Stop() will produce incorrect results for the first runtime.

Safe usage: use extra_module_paths_utf8 only when at most one runtime is active at a time. A per-environment module resolution API is planned for a future version.

env_block_utf8 — double-NUL-terminated block, same as Windows SetEnvironmentStrings format:

var Block: AnsiString;
Block := 'KEY1=val1' + #0 + 'KEY2=val2' + #0 + #0;  // final double-NUL
Cfg.env_block_utf8 := PAnsiChar(Block);

Each KEY=VALUE entry is separated by a single NUL byte; the block ends with an additional NUL (i.e., two consecutive NUL bytes at the end). nil inherits the parent-process environment.

TDNNpmConfig

Passed to INodeNpm methods.

Field Type Description
size Cardinal Required: SizeOf(TDNNpmConfig)
work_dir_utf8 PAnsiChar Working directory; nil → runtime scripts_path
env_block_utf8 PAnsiChar Env block (same format as TDNRuntimeConfig); nil = inherit
timeout_ms Cardinal npm process timeout in ms; 0 = built-in default 300 000 ms (5 min)
flags Cardinal Reserved; 0

TDNLogRecord

Returned by INodeLog.Poll and passed to the callback.

Field Type Description
timestamp_ms UInt64 Milliseconds since epoch
sequence UInt64 Monotonic sequence number
level Cardinal DN_LOG_* constant
thread_id Cardinal OS thread ID
source_utf8 PAnsiChar Component, e.g. 'runtime', 'npm.stdout'
message_utf8 PAnsiChar UTF-8 message

Constants and HRESULT Codes

Log levels

Constant Value Description
DN_LOG_TRACE 0 Very detailed trace
DN_LOG_DEBUG 1 Debug messages
DN_LOG_INFO 2 Informational
DN_LOG_WARN 3 Warnings
DN_LOG_ERROR 4 Errors
DN_LOG_FATAL 5 Fatal errors

Module format

Constant Value Description
DN_MODULE_FORMAT_AUTO 0 Auto by extension (.mjs→ESM, .cjs→CJS)
DN_MODULE_FORMAT_COMMONJS 1 Force CJS
DN_MODULE_FORMAT_ESM 2 Force ESM

Runtime flags

Constant Value Description
DN_RUNTIME_ALLOW_VIRTUAL_ESM_RESOURCE $00000001 Allow ESM from RunScriptText

DN_E_* HRESULT codes

Constant Value Description
DN_E_SCRIPT_EXCEPTION $80040001 JS threw an unhandled exception
DN_E_INVALID_STATE $80040002 Invalid operation for current state
DN_E_RUNTIME_STOPPED $80040003 Runtime stopped, cannot restart
DN_E_INVALID_ARG $80040004 Invalid argument
DN_E_CALL_TIMEOUT $80040005 INodeBridge.CallFunction — timeout expired
DN_E_CALL_FAILED $80040006 INodeBridge.CallFunction — JS threw exception/rejection
DN_E_NOT_REGISTERED $80040007 INodeBridge.CallFunction — function not found on globalThis

INodeBridge Flags

Constant Value Description
DN_EXPORT_WORKER 0 IDispatch::Invoke is called on the event loop thread
DN_EXPORT_MAIN 1 Call is marshaled via WM_DN_DISPATCH to the Delphi thread (hwnd)
WM_DN_DISPATCH WM_USER + $D11 Windows message for dispatch via TDNMainDispatcher

Release Order

INodeLog, INodeNpm, INodeBridge are obtained via QI on the same COM object as INodeRuntime. They all implement COM refcounting (AddRef/Release): the object self-destructs when the last reference drops to zero.

Recommended pattern — Shutdown first:

// After Shutdown, V8 is cleanly stopped; Bridge, Npm, Log may be released in any order.
NodeCheck(Runtime.Shutdown(10000), Factory);  // Stop + WaitForWorkerThread
Bridge  := nil;  // Release; if refcount > 0, object stays alive but detached (CallFunction → DN_E_INVALID_STATE)
Log     := nil;
Npm     := nil;
Runtime := nil;  // Release runtime
Factory := nil;  // Release factory (last)

Classic pattern — Stop + Join:

// Explicit release order for derived interfaces is not required if Shutdown was called first.
// Without Shutdown — release derived interfaces BEFORE Runtime:
Log  := nil;
Npm  := nil;
Bridge := nil;
Runtime.Stop;
Runtime.Join(5000, nil);
Runtime := nil;
Factory := nil;

Why Shutdown matters: during Shutdown, the worker thread resets all V8 Globals (v8::Global<Context>, v8::Global<Promise::Resolver>) inside v8::Locker before releasing the Locker. This guarantees that ~TNodeBridge (called when the last COM reference drops — possibly after iso->Dispose()) does not touch dead V8 objects. The release order of Delphi variables does not matter in this case.


See Also


Thread-affinity reference

All vtable methods are callable from any thread with respect to internal native state. However, the following distinctions apply:

Method Caller thread Notes
INodeFactory::CreateRuntime Any Thread-safe singleton
INodeRuntime::Start Any, once Sets up OS worker thread
INodeRuntime::Stop Any Signals stop; non-blocking
INodeRuntime::Join Any (not worker) Blocks until worker exits
INodeRuntime::Shutdown Any (not worker) Stop + Join in one call
INodeLog::Poll Any Lock-free ring buffer
INodeLog::SetCallback Any Callback fired on worker thread
INodeBridge::CallFunction Any (not worker) Blocks caller thread; worker-thread re-entrant call causes deadlock
INodeBridge::PostFunction Any Non-blocking; callback on main thread (hwnd) or worker thread (fallback)
INodeBridge::Evaluate Any (not worker) Same threading rules as CallFunction
INodeBridge::RegisterExport Any Safe to call before Start
INodeBridge::ProcessMainThreadPending Main thread only Pumps Direction-B callbacks; call from Delphi message loop
INodeBridge::AllocSharedBuffer Any (not worker) Allocates V8 SAB; blocks until V8 allocation completes

COM apartment requirements:

  • DN_EXPORT_WORKER handlers are invoked on the Node.js worker thread (MTA). Registered IDispatch objects must be free-threaded or GIT-marshaled. STA-only objects (standard Delphi TAutoObject) will fail with COM apartment errors.
  • DN_EXPORT_MAIN handlers are dispatched on the Delphi main thread via WM_DN_DISPATCH. VCL-bound objects are safe to use here.

Process-wide side effects:

  • INodeFactory::Release (last reference) — terminal V8 platform shutdown; cannot re-initialize in same process.
  • INodeRuntime::Start — sets NODE_PATH, NODE_EXEC_PATH, UV_THREADPOOL_SIZE env vars process-wide for the duration of the runtime's lifetime.