Files
node/test/parallel/test-child-process-advanced-serialization-host-objects.js
Yagiz Nizipli a1074b8460 child_process: serialize advanced IPC messages natively
The `advanced` IPC serialization codec was implemented in JavaScript
(ChildProcessSerializer / ChildProcessDeserializer in
lib/internal/child_process/serialization.js). It allocated a wrapper
serializer/deserializer per message and crossed the JS/C++ boundary
several times for every message (writeHeader, writeValue, releaseBuffer,
readHeader, readValue and friends).

Move the codec into a native `ipc_serdes` binding that drives the V8
ValueSerializer/ValueDeserializer with a C++ delegate. The wire format
is preserved byte-for-byte: a big-endian uint32 length prefix followed
by the V8 payload, with ArrayBufferViews tagged as host objects so that
Node Buffers round-trip as Buffers rather than plain Uint8Arrays. The
JSON codec is left unchanged.

A cctest (test/cctest/test_node_ipc_serdes.cc) exercises the binding
directly, covering round-trips of primitives, objects, typed arrays and
Buffers (including the Buffer-vs-Uint8Array distinction) and asserting
the big-endian length-prefix framing.

Round-trip throughput
(benchmark/child_process/child-process-ipc-roundtrip):

  payload   before     after      change
  64 B      ~300k/s    ~800k/s    +166%
  1 KiB     ~272k/s    ~616k/s    +126%
  16 KiB    ~91k/s     ~120k/s    +32%
  64 KiB    ~30k/s     ~35k/s     +16%

The gain is largest for small messages, where per-message JavaScript
overhead dominated, and tapers for large messages, where the actual
serialization (already native) dominates.

Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/63933
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
2026-06-18 18:14:46 +00:00

124 lines
3.7 KiB
JavaScript

// Flags: --expose-internals
'use strict';
// Regression tests for the native `advanced` IPC codec (see PR #63933).
// Host-object classification and tag validation must match the previous
// JavaScript (v8.DefaultSerializer-based) behavior exactly.
const common = require('../common');
const assert = require('assert');
const { fork } = require('child_process');
const v8 = require('v8');
const { MessageChannel } = require('worker_threads');
const { internalBinding } = require('internal/test/binding');
if (process.argv[2] === 'inspect') {
process.on('message', (value) => {
process.send({
isBuffer: Buffer.isBuffer(value),
keys: Object.keys(value),
visible: value?.visible,
});
});
return;
}
function inspect(value) {
return new Promise((resolve, reject) => {
const child = fork(__filename, ['inspect'], {
serialization: 'advanced',
stdio: ['ignore', 'ignore', 'inherit', 'ipc'],
});
child.once('message', (message) => {
child.disconnect();
resolve(message);
});
try {
child.send(value);
} catch (err) {
child.disconnect();
reject(err);
}
});
}
async function main() {
// 1) Spreading a non-ArrayBufferView host object must copy own properties
// with [[DefineOwnProperty]] semantics, i.e. without invoking inherited
// setters (the JS codec used `{ ...object }`).
{
const { port1, port2 } = new MessageChannel();
port1.visible = 1;
Object.defineProperty(Object.prototype, 'visible', {
configurable: true,
get() { return undefined; },
set() { throw new Error('setter called'); },
});
let message;
try {
message = await inspect(port1);
} finally {
delete Object.prototype.visible;
port1.close();
port2.close();
}
assert.strictEqual(message.visible, 1);
assert.strictEqual(message.keys[0], 'visible');
}
// 2) A Buffer whose `.constructor` is reassigned is classified by its
// constructor, exactly like v8.DefaultSerializer: it is no longer a
// Buffer on the receiving side.
{
const buf = Buffer.from('abc');
buf.constructor = Uint8Array;
const message = await inspect(buf);
assert.strictEqual(message.isBuffer, false);
}
// 3) Conversely, a Uint8Array with `.constructor === Buffer` round-trips as
// a Buffer.
{
const uint8 = new Uint8Array([1, 2, 3]);
uint8.constructor = Buffer;
const message = await inspect(uint8);
assert.strictEqual(message.isBuffer, true);
}
// 4) Tampering with Buffer.prototype.constructor must not fool the
// classifier: a genuine Buffer is then classified by its (now non-Buffer)
// constructor, matching v8.DefaultSerializer, which compares against a
// stable Buffer reference rather than re-reading Buffer.prototype.
{
const original = Buffer.prototype.constructor;
Buffer.prototype.constructor = Uint8Array;
let message;
try {
message = await inspect(Buffer.from('abc'));
} finally {
Buffer.prototype.constructor = original;
}
assert.strictEqual(message.isBuffer, false);
}
// 5) A host-object tag other than the two the codec emits (0 and 1) must be
// rejected rather than silently accepted.
{
const { deserialize } = internalBinding('ipc_serdes');
class BadTagSerializer extends v8.DefaultSerializer {
_writeHostObject(value) {
this.writeUint32(2); // Valid tags are only 0 or 1
return super._writeHostObject(value);
}
}
const ser = new BadTagSerializer();
ser.writeHeader();
ser.writeValue(Buffer.from('x'));
const payload = ser.releaseBuffer();
assert.throws(() => deserialize(payload), { code: 'ERR_INVALID_STATE' });
}
}
main().then(common.mustCall());