I have been recently rewriting the Native Messaging hosts I’ve written and contributed to developing to support 64 MiB processing.
This is the Native Messaging protocol
Native messaging protocol (Chrome Developers)
Chrome starts each native messaging host in a separate process and communicates with it using standard input (
stdin) and standard output (stdout). The same format is used to send messages in both directions; each message is serialized using JSON, UTF-8 encoded and is preceded with 32-bit message length in native byte order. The maximum size of a single message from the native messaging host is 1 MB, mainly to protect Chrome from misbehaving native applications. The maximum size of the message sent to the native messaging host is 64 MiB.
So far I have completed 64 MiB processing support for Node.js (the same code can be used for Deno and Bun), QuickJS, Bytecode Alliance’s Javy, AssemblyScript, Rust.
I still have C, C++, Static Hermes, Bash, V8, SpiderMonkey, Amazon Web Services LLRT, txiki.js, and Python to do to complete 64 MiB support for the languages, engines, runtimes I’ve written that currently have 1 MiB support.
I started rewriting the algorithm parsing the JSON manually (encoded in u8) in AssemblyScript because there’s no JSON global object. I then ported that algorithm to QuickJS. Somewhere along the way somebody on Discord posted the algorithm I wrote to Google’s Gemini program. The performance improvements to the algorithm are clearly measurable using the code Gemini program spit out.
Instead of asking Google Gemini to spit out the algorithm I wrote “optimized“ in Python, I’ll ask the humans who write Python, first.
Here’s what I have right now, that only handles 1 MiB of input
#!/usr/bin/env -S python3 -u
# https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging
# https://github.com/mdn/webextensions-examples/pull/157
# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.
import sys
import json
import struct
import traceback
try:
# Python 3.x version
# Read a message from stdin and decode it.
def getMessage():
rawLength = sys.stdin.buffer.read(4)
# if len(rawLength) == 0:
# sys.exit(0)
messageLength = struct.unpack('@I', rawLength)[0]
message = sys.stdin.buffer.read(messageLength).decode('utf-8')
return json.loads(message)
# Encode a message for transmission,
# given its content.
def encodeMessage(messageContent):
# https://stackoverflow.com/a/56563264
# https://docs.python.org/3/library/json.html#basic-usage
# To get the most compact JSON representation, you should specify
# (',', ':') to eliminate whitespace.
encodedContent = json.dumps(messageContent, separators=(',', ':')).encode('utf-8')
encodedLength = struct.pack('@I', len(encodedContent))
return {'length': encodedLength, 'content': encodedContent}
# Send an encoded message to stdout
def sendMessage(encodedMessage):
sys.stdout.buffer.write(encodedMessage['length'])
sys.stdout.buffer.write(encodedMessage['content'])
sys.stdout.buffer.flush()
while True:
receivedMessage = getMessage()
sendMessage(encodeMessage(receivedMessage))
except Exception as e:
sys.stdout.buffer.flush()
sys.stdin.buffer.flush()
# https://discuss.python.org/t/how-to-read-1mb-of-input-from-stdin/22534/14
with open('nm_python.log', 'w', encoding='utf-8') as f:
traceback.print_exc(file=f)
sys.exit(0)
This is what I do in QuickJS - including the “optimization“ that Google Gemini spit out
#!/usr/bin/env -S /home/user/bin/qjs -m --std
// QuickJS Native Messaging host
// guest271314, 5-6-2022
function getMessage() {
const header = new Uint32Array(1);
std.in.read(header.buffer, 0, 4);
const output = new Uint8Array(header[0]);
const len = std.in.read(output.buffer, 0, output.length);
return output;
}
//
function sendMessage(message) {
// Constants for readability
const COMMA = 44;
const OPEN_BRACKET = 91; // [
const CLOSE_BRACKET = 93; // ]
const CHUNK_SIZE = 1024 * 1024; // 1MB
// If small enough, send directly (Native endianness handling recommended)
if (message.length <= CHUNK_SIZE) {
const header = new Uint8Array(4);
header[0] = (message.length >> 0) & 0xff;
header[1] = (message.length >> 8) & 0xff;
header[2] = (message.length >> 16) & 0xff;
header[3] = (message.length >> 24) & 0xff;
// Two writes are often better than allocating a new joined buffer
// if the engine supports it. If not, combine them.
const output = new Uint8Array(4 + data.length);
output.set(header, 0);
output.set(data, 4);
std.out.write(output.buffer, 0, output.length);
std.out.flush();
return;
}
let index = 0;
// Iterate through the message until we reach the end
while (index < message.length) {
let splitIndex;
// 1. Determine where to cut the chunk
// Try to jump forward 1MB
let searchStart = index + CHUNK_SIZE - 8;
if (searchStart >= message.length) {
// We are near the end, take everything remaining
splitIndex = message.length;
} else {
// Find the next safe comma to split on
splitIndex = message.indexOf(COMMA, searchStart);
if (splitIndex === -1) {
splitIndex = message.length; // No more commas, take the rest
}
}
// 2. Extract the raw chunk (No copy yet, just a view)
const rawChunk = message.subarray(index, splitIndex);
// 3. Prepare the final payload buffer
// We calculate size first to allocate exactly once per chunk
const startByte = rawChunk[0];
const endByte = rawChunk[rawChunk.length - 1];
let prepend = null;
let append = null;
// Logic to ensure every chunk is a valid JSON array [...]
// Case A: Starts with '[' (First chunk), needs ']' at end if not present
if (startByte === OPEN_BRACKET && endByte !== CLOSE_BRACKET) {
append = CLOSE_BRACKET;
} // Case B: Starts with ',' (Middle chunks), needs '[' at start
else if (startByte === COMMA) {
prepend = OPEN_BRACKET;
// If it doesn't end with ']', it needs one
if (endByte !== CLOSE_BRACKET) {
append = CLOSE_BRACKET;
}
// Note: We skip the leading comma in the raw copy later by offsetting
}
// 4. Construct the output buffer
// Calculate final length: Header (4) + (Prepend?) + Body + (Append?)
// Note: If startByte was COMMA, we usually want to overwrite it with '[',
// but your original logic kept the comma data or shifted.
// Standard approach:
// If raw starts with comma, we replace comma with '[' or insert '['?
// Your logic: Replaced [0] if it was comma.
// Optimized construction based on your logic pattern:
let bodyLength = rawChunk.length;
let payloadOffset = 4; // Start after 4-byte header
// Adjust sizes based on brackets
const hasPrepend = prepend !== null;
const hasAppend = append !== null;
// Special handling for the "Comma Start" case to match your logic:
// Your logic: x[0] = 91; x[i] = data[i]. Effectively replaces comma with '['
let sourceOffset = 0;
if (startByte === COMMA) {
sourceOffset = 1; // Skip the comma from source
bodyLength -= 1; // Reduce source len
// We implicitly assume we prepend '[' in this slot
}
const totalLength = 4 + (hasPrepend ? 1 : 0) + bodyLength +
(hasAppend ? 1 : 0);
const output = new Uint8Array(totalLength);
// Write Length Header (Little Endian example)
const dataLen = totalLength - 4;
output[0] = (dataLen >> 0) & 0xff;
output[1] = (dataLen >> 8) & 0xff;
output[2] = (dataLen >> 16) & 0xff;
output[3] = (dataLen >> 24) & 0xff;
// Write Prepend (e.g. '[')
let cursor = 4;
if (hasPrepend) {
output[cursor] = prepend;
cursor++;
} else if (startByte === COMMA) {
// If we didn't flag prepend but stripped comma, likely need bracket
// Based on your specific logic "x[0] = 91", we treat that as a prepend
output[cursor] = OPEN_BRACKET;
cursor++;
}
// Write Body (Fast copy)
// We use .set() which is much faster than a loop
output.set(rawChunk.subarray(sourceOffset), cursor);
cursor += bodyLength;
// Write Append (e.g. ']')
if (hasAppend) {
output[cursor] = append;
}
// 5. Send immediately
std.out.write(output.buffer, 0, output.length);
std.out.flush();
// Force GC only occasionally if needed (every chunk is often too frequent)
std.gc();
// Move index for next iteration
index = splitIndex;
}
}
function main() {
while (true) {
const message = getMessage();
sendMessage(message);
}
}
try {
main();
} catch (e) {
// std.writeFile(“err.txt”, e.message);
std.exit(0);
}
which is based on this code I wrote in QuickJS
function sendMessage(message) {
if (message.length > 1024 ** 2) {
const json = message;
const data = new Array();
let fromIndex = 1024 ** 2 - 8;
let index = 0;
let i = 0;
do {
i = json.indexOf(44, fromIndex);
const arr = json.subarray(index, i);
data.push(arr);
index = i;
fromIndex += 1024 ** 2 - 8;
} while (fromIndex < json.length);
if (index < json.length) {
data.push(json.subarray(index));
}
for (let j = 0; j < data.length; j++) {
const start = data[j][0];
const end = data[j][data[j].length - 1];
if (start === 91 && end !== 44 && end !== 93) {
const x = new Uint8Array(data[j].length + 1);
for (let i2 = 0; i2 < data[j].length; i2++) {
x[i2] = data[j][i2];
}
x[x.length - 1] = 93;
data[j] = x;
}
if (start === 44 && end !== 93) {
const x = new Uint8Array(data[j].length + 1);
x[0] = 91;
for (let i2 = 1; i2 < data[j].length; i2++) {
x[i2] = data[j][i2];
}
x[x.length - 1] = 93;
data[j] = x;
}
if (start === 44 && end === 93) {
const x = new Uint8Array(data[j].length);
x[0] = 91;
for (let i2 = 1; i2 < data[j].length; i2++) {
x[i2] = data[j][i2];
}
data[j] = x;
}
}
for (let k = 0; k < data.length; k++) {
const arr = data[k];
const header = Uint32Array.from(
{
length: 4,
},
(_, index) => (arr.length >> (index * 8)) & 0xff,
);
const output = new Uint8Array(header.length + arr.length);
output.set(header, 0);
output.set(arr, 4);
std.out.write(output.buffer, 0, output.length);
std.out.flush();
std.gc();
}
} else {
const header = Uint32Array.from({
length: 4,
}, (_, index) => (message.length >> (index * 8)) & 0xff);
const output = new Uint8Array(header.length + message.length);
output.set(header, 0);
output.set(message, 4);
std.out.write(output.buffer, 0, output.length);
std.out.flush();
std.gc();
}
}
How would you write the above algorithm in Python?