";
+ if (!isCtrl && !isAlt) return char;
+ let mod = "";
+ if (isCtrl) mod += "C-";
+ if (isAlt) mod += "A-";
+ return `<${mod}${char}>`;
+ }
+ return null;
}
function main() {
- const transport = new WorkerTransport('nvim-worker.js', { cols: 80, rows: 24 });
- transport.onStatus(text => { setStatus(text); log('[status]', text); });
+ const transport = new WorkerTransport("nvim-worker.js", {
+ cols: 120,
+ rows: 40,
+ });
+ // internal FS.write/stderr trace line the worker emits.
+ transport.onStatus((text) => {
+ log("[status]", text);
+ if (/^(FS\.write|WRITE HOOK|\[stderr\])/.test(text)) return;
+ setStatus(text);
+ });
const nvim = new RpcClient(transport);
- setTimeout(async () => {
- console.log("Auto Request");
+ window.nvim = nvim; // for console debugging
- try {
- const r = await nvim.request("nvim_get_api_info", []);
- console.log(r);
- } catch (e) {
- console.error(e);
+ const uiState = new UiState(120, 40);
+ const gridEl = document.getElementById("grid");
+ const modeEl = document.getElementById("mode");
+
+ let apiInfoPromise = null;
+ function ensureHandleTypesRegistered() {
+ if (!apiInfoPromise) {
+ apiInfoPromise = (async () => {
+ const result = await nvim.request("nvim_get_api_info", []);
+ log("RESPONSE nvim_get_api_info ->", result);
+
+ const [channelId, metadata] = result;
+ log("types metadata ->", metadata.types);
+
+ registerNvimHandleTypes(metadata.types);
+ setStatus("got nvim_get_api_info response, handle types registered");
+
+ return result;
+ })();
}
-}, 10000);
+ return apiInfoPromise;
+ }
- nvim.on('redraw', params => log('Notification redraw', params));
-
- document.getElementById('apiInfoBtn').addEventListener('click', async () => {
- setStatus('sending nvim_get_api_info...');
- try {
- const result = await nvim.request('nvim_get_api_info', []);
- log('RESPONSE nvim_get_api_info ->', result);
- setStatus('got nvim_get_api_info response');
- } catch (e) {
- log('ERROR nvim_get_api_info ->', e);
- setStatus('nvim_get_api_info failed, see log');
+ // Auto-attach + visible failure status
+ transport.onReady(async () => {
+ const maxAttempts = 5;
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
+ try {
+ await ensureHandleTypesRegistered();
+ await nvim.request("nvim_ui_attach", [
+ 110,
+ 40,
+ { rgb: true, ext_linegrid: true },
+ ]);
+ setStatus("UI attached");
+ return;
+ } catch (e) {
+ console.error(`auto-attach attempt ${attempt} failed`, e);
+ log("auto-attach failed ->", e && e.message ? e.message : String(e));
+ if (attempt === maxAttempts) {
+ setStatus("auto-attach failed after retries, see console (F12)");
+ } else {
+ await new Promise((r) => setTimeout(r, 300 * attempt));
+ }
+ }
}
});
- document.getElementById('attachBtn').addEventListener('click', async () => {
- setStatus('sending nvim_ui_attach...');
- try {
- const result = await nvim.request('nvim_ui_attach', [80, 24, { rgb: true, ext_linegrid: true }]);
- log('RESPONSE nvim_ui_attach ->', result);
- setStatus('UI attached finally. Watching for redraw notifications...');
- } catch (e) {
- log('ERROR nvim_ui_attach ->', e);
- setStatus('nvim_ui_attach failed, see log');
- }
- });
+ nvim.on("redraw", (params) =>
+ handleRedrawEvents(uiState, gridEl, modeEl, params),
+ );
- document.getElementById('persistBtn').addEventListener('click', () => {
- transport.persist();
+ gridEl.addEventListener("click", () => gridEl.focus());
+ gridEl.addEventListener("keydown", (ev) => {
+ const keys = translateKey(ev);
+ if (!keys) return;
+ ev.preventDefault();
+ nvim
+ .request("nvim_input", [keys])
+ .catch((e) => console.error("nvim_input failed", e));
});
}
+
+if (!crossOriginIsolated) {
+ setStatus(
+ "NOT cross-origin isolated -- SharedArrayBuffer unavailable. Serve with COOP/COEP headers.",
+ );
+} else {
+ main();
+}
diff --git a/src/wasm/index.html b/src/wasm/index.html
index 44b38347a4..db24ceb17e 100644
--- a/src/wasm/index.html
+++ b/src/wasm/index.html
@@ -1,25 +1,53 @@
-
+
-
-
-nvim.wasm RPC client
-
-
-
-
-
-
-
- booting…
-
-
+
+
+ nvim.wasm RPC client
+
-
-
+
+
+
+
+
+
+
+ booting…
+
+
+
+
+
+
+
+
diff --git a/src/wasm/msgpack.js b/src/wasm/msgpack.js
index 52291cb566..a275a1a612 100644
--- a/src/wasm/msgpack.js
+++ b/src/wasm/msgpack.js
@@ -1,25 +1,62 @@
-// Check that the library is actually loaded.
-if (typeof MessagePack === 'undefined') {
+if (typeof msgpackr === 'undefined') {
throw new Error(
- 'MessagePack library not loaded. ' +
- 'Check that the CDN script tag for @msgpack/msgpack is present and loads correctly.'
+ 'msgpackr library not loaded. ' +
+ 'Check that the script tag for msgpackr is present and loads correctly.'
);
}
+// Buffer/Window/Tabpage handles come back as msgpack EXT values.
+// - Type code: not fixed across builds, so read it from
+// nvim_get_api_info()'s types metadata at runtime instead of
+// hardcoding it.
+// - Payload: itself a variable length msgpack integer,
+// so it needs a real unpack call, not a fixed width DataView read.
+function registerNvimHandleTypes(types) {
+ for (const [name, info] of Object.entries(types)) {
+ msgpackr.addExtension({
+ type: info.id,
+ unpack(payload) {
+ return { __nvimType: name, id: msgpackr.unpack(payload) };
+ },
+ pack(value) {
+ return msgpackr.pack(value.id);
+ },
+ });
+ console.log(`[msgpack] registered Nvim EXT type ${info.id} -> ${name}`);
+ }
+}
+
const MsgpackCodec = {
encode(value) {
- const encoded = MessagePack.encode(value);
-
- // Debug: log the encoded bytes (first few bytes) to verify.
+ const encoded = msgpackr.pack(value);
const prefix = Array.from(encoded.slice(0, Math.min(encoded.length, 8)))
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
console.log(`[MsgpackCodec.encode] ${encoded.length} bytes, prefix: ${prefix}`);
-
return encoded;
},
- decodeMulti(bytes) {
- return [...MessagePack.decodeMulti(bytes)]; // ✅ correct global
+ decodeMultiple(bytes) {
+ const messages = [];
+ let consumed = 0;
+
+ try {
+ msgpackr.unpackMultiple(bytes, (value, start, end) => {
+ messages.push(value);
+ consumed = end;
+ });
+ consumed = bytes.length;
+ } catch (e) {
+ if (!isIncompleteDataError(e)) {
+ console.error('[MsgpackCodec.decodeMultiple] fatal decode error', e, 'consumed so far:', consumed);
+ throw e;
+ }
+ }
+
+ return { messages, consumed };
},
};
+
+function isIncompleteDataError(e) {
+ return /unexpected end|incomplete|out of bounds|out of range/i.test(e?.message || '');
+}
diff --git a/src/wasm/protocol.js b/src/wasm/msgpackrpc.js
similarity index 66%
rename from src/wasm/protocol.js
rename to src/wasm/msgpackrpc.js
index b8f1fede9f..11d467d9cb 100644
--- a/src/wasm/protocol.js
+++ b/src/wasm/msgpackrpc.js
@@ -13,22 +13,22 @@ const Protocol = {
},
// classifies a decoded msgpack-rpc array into a tagged object.
- // throws if `msg` doesn't look like a valid rpc message.
+ // throws if msg doesn't look like a valid rpc message.
parseMessage(msg) {
- if (!Array.isArray(msg)) throw new Error('rpc message is not an array');
+ if (!Array.isArray(msg)) throw new Error("rpc message is not an array");
const [type, ...rest] = msg;
if (type === 0) {
const [msgid, method, params] = rest;
- return { kind: 'request', msgid, method, params };
+ return { kind: "request", msgid, method, params };
}
if (type === 1) {
const [msgid, error, result] = rest;
- return { kind: 'response', msgid, error, result };
+ return { kind: "response", msgid, error, result };
}
if (type === 2) {
const [method, params] = rest;
- return { kind: 'notification', method, params };
+ return { kind: "notification", method, params };
}
- throw new Error('unknown rpc message type: ' + type);
+ throw new Error("unknown rpc message type: " + type);
},
};
diff --git a/src/wasm/nvim-worker.js b/src/wasm/nvim-worker.js
index 07b9cbbcf9..f277bf3576 100644
--- a/src/wasm/nvim-worker.js
+++ b/src/wasm/nvim-worker.js
@@ -1,209 +1,432 @@
-const CAP = 1 << 16; // must match the main thread's ring buffer size
-let state, ringData; // state is now Int32Array(3): [head, tail, closed]
+const CAP = 1 << 16;
+
+let state = null;
+let ringData = null;
+
+let moduleRef = null;
-let stdinPollCount = 0;
let totalBytesRead = 0;
const consumedBytes = [];
+const STATUS_MSG_CAP = 2000;
+let statusMsgCount = 0;
+let statusCapNoticeSent = false;
-let pendingPollCallbacks = [];
-function wakePendingPolls() {
- const cbs = pendingPollCallbacks;
- pendingPollCallbacks = [];
- for (const cb of cbs) cb(65);
-}
-let lastCheckedTail = -1;
-
-function checkForNewDataAndWake() {
- if (!state) return; // not initialized yet
- const head = Atomics.load(state, 0);
- const tail = Atomics.load(state, 1);
- if (head !== tail && pendingPollCallbacks.length > 0) {
- wakePendingPolls();
+function safeStatus(text) {
+ statusMsgCount++;
+ if (statusMsgCount > STATUS_MSG_CAP) {
+ if (!statusCapNoticeSent) {
+ statusCapNoticeSent = true;
+ postMessage({
+ type: "status",
+ text:
+ "STATUS LOGGING SUPPRESSED after " +
+ STATUS_MSG_CAP +
+ " messages -- likely a busy/runaway loop. " +
+ "See perFdWriteCount below for which fd is spinning.",
+ });
}
+ return;
+ }
+ postMessage({ type: "status", text: text });
}
-setInterval(checkForNewDataAndWake, 20); // check every 20ms
-
-function popNonBlocking() {
- const head = Atomics.load(state, 0);
- const tail = Atomics.load(state, 1);
- if (head !== tail) {
- const b = ringData[tail];
- Atomics.store(state, 1, (tail + 1) % CAP);
- totalBytesRead++;
- consumedBytes.push(b);
- return b;
- }
- return -1;
+const perFdWriteCount = {};
+function bumpFdWriteCount(fd) {
+ perFdWriteCount[fd] = (perFdWriteCount[fd] || 0) + 1;
}
-// blocks the Worker thread until a byte is available or shutdown fires.
-function popBlocking() {
- while (true) { //producer-consumer
- const head = Atomics.load(state, 0);
- const tail = Atomics.load(state, 1);
+/* File descriptor monitored by libuv for stdin. Verify this value
+ against the startup FD dump, as it may differ across environments. */
- if (head !== tail) {
- const b = ringData[tail];
- Atomics.store(state, 1, (tail + 1) % CAP);
- totalBytesRead++;
- consumedBytes.push(b);
- return b;
- }
+const STDIN_FD = 9;
- if (Atomics.load(state, 2) === 1) {
- return -1;
- }
+/*
+ SharedArrayBuffer layout:
+ offset 0:
+ Int32Array:
+ [0] head
+ [1] tail
+ [2] closed
- // Blocks here until Atomics.notify(state, 0) fires, or 1s elapses
- Atomics.wait(state, 0, head, 1000);
- }
+ offset 12:
+ Uint8Array data
+*/
+
+function unreadCount() {
+ const head = Atomics.load(state, 0);
+ const tail = Atomics.load(state, 1);
+ return (head - tail + CAP) % CAP;
}
-let stdoutBuf = [];
-let stderrBuf = [];
-
-function flushStdout() {
- if (stdoutBuf.length) {
- postMessage({ type: 'stdout', bytes: stdoutBuf });
- stdoutBuf = [];
+// Synchronizes the JS ring buffer with the shared atomics used by
+// uv__io_poll to allow libuv to detect newly available input.
+function markReadable() {
+ safeStatus(
+ "MARK READABLE called moduleRef=" + !!moduleRef + " STDIN_FD=" + STDIN_FD,
+ );
+ if (!moduleRef) return;
+ const n = unreadCount();
+ safeStatus("MARK READABLE n=" + n);
+ try {
+ moduleRef.ccall(
+ "uv_browser_set_readable",
+ null,
+ ["number", "number"],
+ [STDIN_FD, n],
+ );
+ safeStatus("MARK READABLE ccall succeeded");
+ } catch (e) {
+ safeStatus("MARK READABLE FAILED: " + e);
}
}
-function flushStderr() {
- if (stderrBuf.length) {
- postMessage({ type: 'stderr', bytes: stderrBuf });
- stderrBuf = [];
+// Debug helper
+function dumpFD(fd) {
+ const s = moduleRef.FS.streams[fd];
+
+ safeStatus(" FD " + fd);
+
+ if (!s) {
+ safeStatus("NO STREAM");
+ return;
}
+
+ safeStatus("path=" + s.path + " fd=" + s.fd + " tty=" + !!s.tty);
+
+ safeStatus(
+ "same stdout ops=" + (s.stream_ops === moduleRef.FS.streams[1].stream_ops),
+ );
+
+ try {
+ const st = moduleRef.FS.fstat(fd);
+
+ safeStatus(
+ "mode=" +
+ st.mode.toString(8) +
+ " isChrdev=" +
+ moduleRef.FS.isChrdev(st.mode),
+ );
+ } catch (e) {
+ safeStatus("fstat failed: " + e);
+ }
+}
+
+function popByte() {
+ const head = Atomics.load(state, 0);
+ const tail = Atomics.load(state, 1);
+
+ if (head === tail) return -1;
+
+ const b = ringData[tail];
+
+ Atomics.store(state, 1, (tail + 1) % CAP);
+
+ totalBytesRead++;
+ consumedBytes.push(b);
+
+ console.log("POP BYTE", b.toString(16));
+
+ return b;
+}
+
+// Called when new bytes are pushed into the ring buffer from the main
+// thread. Advances head and notifies both the JS side Atomics.wait() consumers and the C-side
+// futex consumers (uv__io_poll).
+function pushBytes(bytes) {
+ if (!bytes || !bytes.length) return;
+
+ let head = Atomics.load(state, 0);
+
+ for (let i = 0; i < bytes.length; i++) {
+ const tail = Atomics.load(state, 1);
+ const nextHead = (head + 1) % CAP;
+
+ /* If the ring buffer is full, drop new data instead of overwriting
+ unread bytes. Overwriting would corrupt the input stream. If this
+ happens often, increase CAP or add backpressure on the producer. */
+ if (nextHead === tail) {
+ console.warn("stdin ring buffer full, dropping byte");
+ break;
+ }
+
+ ringData[head] = bytes[i];
+ head = nextHead;
+ }
+
+ Atomics.store(state, 0, head);
+
+ // wake anything doing an Atomics.wait() on head, if anything still does
+ Atomics.notify(state, 0);
+
+ // tell the C side (uv__io_poll) that fd 9 now has data
+ markReadable();
}
function flushAll() {
- flushStdout();
- flushStderr();
+ if (stdoutBuffer.length) {
+ postMessage({
+ type: "stdout",
+ bytes: stdoutBuffer,
+ });
+ stdoutBuffer = [];
+ }
+
+ if (stderrBuffer.length) {
+ postMessage({
+ type: "stderr",
+ bytes: stderrBuffer,
+ });
+ stderrBuffer = [];
+ }
}
-function writeStdout(c) {
- stdoutBuf.push(c);
- flushStdout();
-}
+let stdoutBuffer = [];
+let stderrBuffer = [];
function writeStderr(c) {
- stderrBuf.push(c);
- if (stderrBuf.length > 80) {
- flushStderr();
+ stderrBuffer.push(c);
+
+ if (stderrBuffer.length > 50) {
+ postMessage({
+ type: "stderr",
+ bytes: stderrBuffer,
+ });
+
+ stderrBuffer = [];
}
}
function makeArgv(M, args) {
- const ptrs = args.map(s => {
- const len = M.lengthBytesUTF8(s) + 1, p = M._malloc(len);
+ const ptrs = args.map((s) => {
+ const len = M.lengthBytesUTF8(s) + 1;
+
+ const p = M._malloc(len);
+
M.stringToUTF8(s, p, len);
+
return p;
});
+
const argv = M._malloc((ptrs.length + 1) * 4);
- ptrs.forEach((p, i) => M.setValue(argv + i * 4, p, '*'));
- M.setValue(argv + ptrs.length * 4, 0, '*');
- return { argc: ptrs.length, argv };
+
+ ptrs.forEach((p, i) => M.setValue(argv + i * 4, p, "*"));
+
+ M.setValue(argv + ptrs.length * 4, 0, "*");
+
+ return {
+ argc: ptrs.length,
+ argv,
+ };
}
-let moduleRef = null;
-
self.onerror = (e) => {
- postMessage({ type: 'status', text: 'WORKER ERROR: ' + e.message });
+ safeStatus("WORKER ERROR " + e.message);
};
self.onunhandledrejection = (e) => {
- postMessage({ type: 'status', text: 'WORKER REJECTION: ' + e });
+ const r = e.reason;
+ const info =
+ r && typeof r === "object"
+ ? `name=${r.name} message=${r.message} status=${r.status}`
+ : String(r);
+ safeStatus("REJECTION " + info);
};
-function makeBridgeReadOps(origOps, label) {
- const newOps = Object.assign({}, origOps);
- newOps.read = function (stream, buffer, offset, length, position) {
- let n = 0;
- while (n < length) {
- const b = popNonBlocking();
- if (b === -1) break;
- buffer[offset + n] = b;
- n++;
- }
- if (n === 0) {
- console.log(`[stream_ops:${label}] no data available, returning 0 (temporary — not correct long-term)`);
- return 0; // TEMP: fake EOF
- }
- console.log(`[stream_ops:${label}] read ${n} bytes`);
- return n;
- };
-newOps.poll = function (stream, timeout, notifyCallback) {
- const head = Atomics.load(state, 0);
- const tail = Atomics.load(state, 1);
+/* Hook the shared TTY get_char callback instead of stream_ops.read.
+ All duplicated file descriptors share the same TTY object, making
+ this work for stdin and any dup()ed descriptors.
+ Return one byte when available, undefined when no data is ready
+ (causing Emscripten to report EAGAIN), or null on EOF. Blocking is
+ handled by uv__io_poll so this callback always remains non-blocking.
+ */
+function installStdoutWrite(m, stdoutStream) {
+ if (
+ !stdoutStream.stream_ops ||
+ typeof stdoutStream.stream_ops.write !== "function"
+ ) {
+ throw new Error(
+ "stdout stream has no stream_ops.write: " + stdoutStream.path,
+ );
+ }
+ // The worker owns the transport, so forwarding the chunk via postMessage
+ // completes the write.
+ // Skip the original stream_ops.write to avoid duplicate output.
+ stdoutStream.stream_ops.write = function (
+ stream,
+ buffer,
+ offset,
+ length,
+ position,
+ ) {
+ safeStatus(
+ "WRITE HOOK fd=" + stream.fd + " path=" + stream.path + " len=" + length,
+ );
- if (head !== tail) {
- return 65;
+ try {
+ console.log("HOOK: entered");
+ /* Forward the entire chunk as a single message to preserve msgpack-RPC
+ framing. Then copy it into a Uint8Array to ensure the transmitted bytes
+ remain correct regardless of the source buffer's signedness. */
+ const chunk = new Uint8Array(length);
+ for (let i = 0; i < length; i++) {
+ chunk[i] = buffer[offset + i];
+ }
+ console.log(
+ "HOOK: chunk built",
+ chunk.length,
+ chunk[0],
+ chunk[1],
+ chunk[2],
+ chunk[3],
+ );
+ console.log(
+ "stdout chunk",
+ length,
+ chunk[0],
+ chunk[1],
+ chunk[2],
+ chunk[3],
+ );
+ console.log("HOOK: BEFORE postMessage");
+
+ postMessage({
+ type: "stdout",
+ bytes: chunk,
+ });
+ console.log("HOOK: AFTER postMessage");
+ } catch (e) {
+ console.error("HOOK: postMessage failed", e);
+
+ safeStatus("STDOUT HOOK ERROR: " + e + " stack=" + e.stack);
+ }
+ return length;
+ };
+}
+
+function installStdinGetChar(m, stdinStream) {
+ if (!stdinStream.tty || !stdinStream.tty.ops) {
+ throw new Error(
+ "stdin stream has no .tty.ops -- stream is not TTY-backed: " +
+ stdinStream.path,
+ );
+ }
+
+ stdinStream.tty.ops.get_char = function (tty) {
+ const b = popByte();
+
+ if (b !== -1) {
+ markReadable(); // reflect the new unread count
+ return b;
}
- if (notifyCallback) {
- pendingPollCallbacks.push(notifyCallback);
+ if (Atomics.load(state, 2) === 1) {
+ console.log("stdin closed");
+ return null; // real EOF
}
- return 0;
-};
- const origGetattr = origOps.getattr;
- newOps.getattr = function (stream) {
- const attr = origGetattr ? origGetattr.call(this, stream) : { mode: 0 };
- attr.mode = (attr.mode & ~0xF000) | 0x1000;
- return attr;
+ return undefined; // no data yet -> Emscripten turns this into EAGAIN
};
- return newOps;
}
self.onmessage = async (ev) => {
const msg = ev.data;
- if (msg.type === 'init') {
- state = new Int32Array(msg.sab, 0, 3); // [head, tail, closed]
+ if (msg.type === "init") {
+ state = new Int32Array(msg.sab, 0, 3); // [head, tail, closed]
ringData = new Uint8Array(msg.sab, 12, CAP); // offset moved from 8 → 12
-setInterval(checkForNewDataAndWake, 20);
- postMessage({ type: 'status', text: 'loading wasm...' });
- importScripts('../../zig-out/bin/nvim.js');
+ safeStatus("loading wasm...");
+ importScripts("../../zig-out/bin/nvim.js");
const m = await createNvim({
- locateFile: (p) => p.endsWith('.data') ? '../../zig-out/bin/nvim.data' : '../../zig-out/bin/' + p,
+ locateFile: (p) =>
+ p.endsWith(".data")
+ ? "../../zig-out/bin/nvim.data"
+ : "../../zig-out/bin/" + p,
noInitialRun: true,
- stdin: () => {
- console.log('[stdin callback] fired');
- const b = popNonBlocking();
- return b === -1 ? null : b;
- },
- stdout: c => writeStdout(c),
- stderr: c => writeStderr(c),
- print: t => postMessage({ type: 'status', text: '[print] ' + t }),
- printErr: t => postMessage({ type: 'status', text: '[printErr] ' + t }),
- preRun: [m => {
- m.ENV.TERM = "xterm-256color";
- m.ENV.HOME = "/home/user";
- m.ENV.VIMRUNTIME = "/runtime";
- m.ENV.COLUMNS = String(msg.cols);
- m.ENV.LINES = String(msg.rows);
- try { m.FS.mkdir('/tmp'); } catch (e) {}
- m.FS.mkdir('/home/user');
- m.FS.mkdir('/home/user/.config');
- m.FS.mkdir('/home/user/.local');
- m.FS.mkdir('/home/user/.local/share');
- m.FS.mount(m.IDBFS, {}, '/home/user/.config');
- m.FS.mount(m.IDBFS, {}, '/home/user/.local/share');
- }],
- });
+ interactive: false,
- await new Promise((res, rej) => m.FS.syncfs(true, e => e ? rej(e) : res()));
- try { m.FS.mkdir('/home/user/.config/nvim'); } catch (e) {}
- try { m.FS.mkdir('/home/user/.local/share/nvim'); } catch (e) {}
- try { m.FS.mkdir('/runtime/parser'); } catch (e) {}
- ['lua', 'c', 'vim', 'vimdoc', 'query', 'markdown', 'markdown_inline'].forEach(p => {
- try { m.FS.writeFile(`/runtime/parser/${p}.so`, ''); } catch (e) {}
+ stdout: () => {},
+
+ stderr: (c) => writeStderr(c),
+ tty: false,
+ print: (t) => safeStatus("[print] " + t),
+ printErr: (t) => safeStatus("[printErr] " + t),
+ preRun: [
+ (m) => {
+ m.ENV.TERM = "xterm-256color";
+ m.ENV.HOME = "/home/user";
+ m.ENV.VIMRUNTIME = "/runtime";
+ m.ENV.NVIM_LOG_FILE = "/home/user/nvim.log";
+ },
+ ],
});
moduleRef = m;
+ const statePtr = m.ccall(
+ "uv_browser_get_shared_state_ptr",
+ "number",
+ [],
+ [],
+ );
+ postMessage({
+ type: "shared-info",
+ memory: m.HEAPU8.buffer,
+ statePtr: statePtr,
+ });
+ postMessage({ type: "ready" });
+ const stdinStream = m.FS.streams[0];
+
+ console.log("stdin stream", stdinStream.path);
+
+ installStdinGetChar(m, stdinStream);
+ const stdoutStream = m.FS.streams[1];
+
+ console.log("stdoutStream ", stdoutStream);
+ console.log(stdoutStream.node);
+ console.log(stdoutStream.node.rdev);
+ console.log(stdoutStream.tty);
+ console.log(stdoutStream.stream_ops);
+
+ console.log("stdout stream", stdoutStream.path);
+ installStdoutWrite(m, stdoutStream);
+ console.log("PATCHED STDOUT WRITE =", m.FS.streams[1].stream_ops.write);
+ await new Promise((res, rej) =>
+ m.FS.syncfs(true, (e) => (e ? rej(e) : res())),
+ );
+ try {
+ m.FS.mkdir("/home/user/.config/nvim");
+ } catch (e) {}
+ try {
+ m.FS.mkdir("/home/user/.local/share/nvim");
+ } catch (e) {}
+ try {
+ m.FS.mkdir("/runtime/parser");
+ } catch (e) {}
+ [
+ "lua",
+ "c",
+ "vim",
+ "vimdoc",
+ "query",
+ "markdown",
+ "markdown_inline",
+ ].forEach((p) => {
+ try {
+ m.FS.writeFile(`/runtime/parser/${p}.so`, "");
+ } catch (e) {}
+ });
+
+ const patchedOps = stdinStream.tty && stdinStream.tty.ops;
+ // Dump every open fd and confirm which fd libuv is actually polling
+ // on and whether it shares the patched tty record. If STDIN_FD
+ // above doesn't match what shows up here as the dup'd stdin fd then
+ // fix the constant at the top of this file.
+ for (let i = 0; i < 16; i++) {
+ const s = m.FS.streams[i];
+ console.log("FD", i, s && s.path, s && s.tty && s.tty.ops === patchedOps);
+ }
+
function sanitizeAttr(attr) {
attr.dev = attr.dev ?? 1;
attr.ino = attr.ino ?? 1;
@@ -212,10 +435,11 @@ setInterval(checkForNewDataAndWake, 20);
attr.uid = attr.uid ?? 0;
attr.gid = attr.gid ?? 0;
attr.rdev = attr.rdev ?? 0;
- attr.size = (typeof attr.size === 'number' && !isNaN(attr.size)) ? attr.size : 0;
+ attr.size =
+ typeof attr.size === "number" && !isNaN(attr.size) ? attr.size : 0;
attr.blksize = attr.blksize ?? 4096;
attr.blocks = attr.blocks ?? 0;
- const validDate = d => d instanceof Date && !isNaN(d.getTime());
+ const validDate = (d) => d instanceof Date && !isNaN(d.getTime());
attr.atime = validDate(attr.atime) ? attr.atime : new Date(0);
attr.mtime = validDate(attr.mtime) ? attr.mtime : new Date(0);
attr.ctime = validDate(attr.ctime) ? attr.ctime : new Date(0);
@@ -223,18 +447,21 @@ setInterval(checkForNewDataAndWake, 20);
}
function isStdinLike(path) {
- const p = path || '';
- return p.startsWith('pipe[') || p.includes('my_stdin') || p === '/dev/stdin';
+ const p = path || "";
+ return (
+ p.startsWith("pipe[") || p.includes("my_stdin") || p === "/dev/stdin"
+ );
}
- ['fstat', 'stat', 'lstat'].forEach(name => {
- if (typeof m.FS[name] !== 'function') return;
+ ["fstat", "stat", "lstat"].forEach((name) => {
+ if (typeof m.FS[name] !== "function") return;
const orig = m.FS[name].bind(m.FS);
m.FS[name] = function (...args) {
const attr = orig(...args);
- const path = (name === 'fstat')
- ? (m.FS.streams[args[0]] && m.FS.streams[args[0]].path)
- : args[0];
+ const path =
+ name === "fstat"
+ ? m.FS.streams[args[0]] && m.FS.streams[args[0]].path
+ : args[0];
if (isStdinLike(path)) {
attr.mode = 0o010666;
}
@@ -242,62 +469,129 @@ setInterval(checkForNewDataAndWake, 20);
};
});
- const origCreateStream = m.FS.createStream;
- m.FS.createStream = function (stream, fd) {
- const s = origCreateStream.call(this, stream, fd);
- const p = s.path || '';
- const looksLikeStdin = p.startsWith('pipe[') || p.includes('my_stdin') || p === '/dev/stdin';
- if (looksLikeStdin && !s._patched) {
- s.stream_ops = makeBridgeReadOps(s.stream_ops, 'createStream:' + p);
- s._patched = true;
- }
- return s;
- };
-
- const origFSRead = m.FS.read;
- m.FS.read = function (stream, buffer, offset, length, position) {
- if (stream.path && stream.path.startsWith('pipe[') && !stream._patched) {
- stream.stream_ops = makeBridgeReadOps(stream.stream_ops, 'fallback:' + stream.path);
- stream._patched = true;
- }
- return origFSRead.call(this, stream, buffer, offset, length, position);
- };
-
const origFSWrite = m.FS.write;
m.FS.write = function (stream, buffer, offset, length, position, canOwn) {
- return origFSWrite.call(this, stream, buffer, offset, length, position, canOwn);
+ console.log("FS.write-----");
+ console.log("fd =", stream.fd);
+ console.log("path =", stream.path);
+
+ console.log("stream_ops =", stream.stream_ops);
+
+ console.log("write fn =", stream.stream_ops && stream.stream_ops.write);
+
+ console.log("stdout write fn =", m.FS.streams[1].stream_ops.write);
+
+ console.log(
+ "same write?",
+ stream.stream_ops &&
+ stream.stream_ops.write === m.FS.streams[1].stream_ops.write,
+ );
+ bumpFdWriteCount(stream.fd);
+ safeStatus(
+ "FS.write fd=" +
+ stream.fd +
+ " path=" +
+ stream.path +
+ " len=" +
+ length +
+ " (fd" +
+ stream.fd +
+ " call#" +
+ perFdWriteCount[stream.fd] +
+ ")",
+ );
+ return origFSWrite.call(
+ this,
+ stream,
+ buffer,
+ offset,
+ length,
+ position,
+ canOwn,
+ );
};
- const { argc, argv } = makeArgv(m, ["nvim", "--embed"]);
- let ret;
- try {
- postMessage({ type: 'status', text: 'Starting Neovim...' });
- ret = await m._nvim_main(argc, argv);
- const headAtExit = Atomics.load(state, 0);
- const tailAtExit = Atomics.load(state, 1);
- const unread = (headAtExit - tailAtExit + CAP) % CAP;
- postMessage({ type: 'status', text: `_nvim_main RETURNED ret=${ret}, unread bytes still in buffer=${unread} (head=${headAtExit}, tail=${tailAtExit})` });
+ const { argc, argv } = makeArgv(m, ["nvim", "--embed", "--clean"]);
+ let ret;
+
+ try {
+ safeStatus("Starting Neovim...");
+
+ console.log("CALLING NVIM MAIN");
+
+ const nvimPromise = m._nvim_main(argc, argv);
+
+ console.log("returned:", nvimPromise);
+ console.log("instanceof Promise:", nvimPromise instanceof Promise);
+ console.log("constructor:", nvimPromise?.constructor?.name);
+
+ setTimeout(() => {
+ dumpFD(1);
+ dumpFD(9);
+ dumpFD(10);
+ }, 1000);
+
+ ret = await nvimPromise;
+
+ console.log("NVIM EXITED");
+ const unread = unreadCount();
+ safeStatus(
+ `_nvim_main RETURNED ret=${ret}, unread bytes still in buffer=${unread}`,
+ );
} catch (e) {
flushAll();
- postMessage({ type: 'status', text: 'EXCEPTION: ' + e.message });
+ safeStatus("EXCEPTION: " + e.message + "\nSTACK:\n" + e.stack);
throw e;
}
flushAll();
- const hexStr = consumedBytes.slice(0, 50).map(x => x.toString(16).padStart(2, '0')).join(' ');
- const remaining = consumedBytes.length > 50 ? `... (${consumedBytes.length - 50} more bytes)` : '';
- postMessage({ type: 'status', text: `EXIT CODE ${ret}. Read ${totalBytesRead} bytes: ${hexStr}${remaining}` });
+ const hexStr = consumedBytes
+ .slice(0, 50)
+ .map((x) => x.toString(16).padStart(2, "0"))
+ .join(" ");
+ const remaining =
+ consumedBytes.length > 50
+ ? `... (${consumedBytes.length - 50} more bytes)`
+ : "";
+ safeStatus(
+ `EXIT CODE ${ret}. Read ${totalBytesRead} bytes: ${hexStr}${remaining}`,
+ );
}
- if (msg.type === 'persist') {
- if (!moduleRef) { postMessage({ type: 'persisted', error: 'module not ready' }); return; }
- moduleRef.FS.syncfs(false, e => postMessage({ type: 'persisted', error: e ? String(e) : null }));
+
+ if (msg.type === "stdin") {
+ pushBytes(msg.bytes);
}
- if (msg.type === 'shutdown') {
+
+ if (msg.type === "persist") {
+ if (!moduleRef) {
+ postMessage({ type: "persisted", error: "module not ready" });
+ return;
+ }
+ moduleRef.FS.syncfs(false, (e) =>
+ postMessage({ type: "persisted", error: e ? String(e) : null }),
+ );
+ }
+ if (msg.type === "readable-hint") {
+ markReadable();
+ }
+
+ if (msg.type === "dump-counters") {
+ safeStatus(
+ "perFdWriteCount=" +
+ JSON.stringify(perFdWriteCount) +
+ " statusMsgCount=" +
+ statusMsgCount +
+ " suppressed=" +
+ statusCapNoticeSent,
+ );
+ }
+ if (msg.type === "shutdown") {
if (state) {
- Atomics.store(state, 2, 1);
- Atomics.notify(state, 0); // wake up any pending popBlocking()
+ Atomics.store(state, 2, 1);
+ Atomics.notify(state, 0); // wake up anything Atomics.wait()ing
+ markReadable(); // also wake the C side futex so uv__io_poll rescans and sees the close
}
}
};
diff --git a/src/wasm/rpc.js b/src/wasm/rpc.js
index 2aa220f434..761ad5a606 100644
--- a/src/wasm/rpc.js
+++ b/src/wasm/rpc.js
@@ -1,81 +1,130 @@
-// Transport: owns the worker + SharedArrayBuffer ring buffer
+// Transport: owns the worker + SharedArrayBuffer
class WorkerTransport {
constructor(workerPath, { cols = 80, rows = 24, cap = 1 << 16 } = {}) {
this.CAP = cap;
- this.sab = new SharedArrayBuffer(12 + this.CAP);
- this.state = new Int32Array(this.sab, 0, 3); // [head, tail, closed]
+ this.sab = new SharedArrayBuffer(12 + this.CAP);
+ this.state = new Int32Array(this.sab, 0, 3); // [head, tail, closed]
this.ringData = new Uint8Array(this.sab, 12, this.CAP); // offset 12
this._bytesHandlers = [];
this._statusHandlers = [];
+ this._readyHandlers = [];
this.worker = new Worker(workerPath);
this.worker.onmessage = (ev) => {
- console.log('[main] Worker message:', ev.data.type, ev.data);
+ console.log("[main] worker message", ev.data.type, ev.data);
this._onWorkerMessage(ev.data);
};
- this.worker.onerror = (e) => this._emitStatus('worker error: ' + e.message);
- this.worker.postMessage({ type: 'init', sab: this.sab, cols, rows });
+ this.worker.onerror = (e) => this._emitStatus("worker error: " + e.message);
+ this.worker.postMessage({ type: "init", sab: this.sab, cols, rows });
}
+
shutdown() {
Atomics.store(this.state, 2, 1);
Atomics.notify(this.state, 0);
- this.worker.postMessage({ type: 'shutdown' });
- }
+ this.worker.postMessage({ type: "shutdown" });
+ }
_onWorkerMessage(msg) {
- console.log('[main] _onWorkermessage:', msg.type);
- if (msg.type === 'stdout') {
+ console.log("[main] _onWorkerMessage got", msg.type);
+
+ if (msg.type === "stdout") {
console.log(`[main] stdout bytes:`, msg.bytes);
- this._bytesHandlers.forEach(h => h(msg.bytes));
- } else if (msg.type === 'stderr') {
+ this._bytesHandlers.forEach((h) => h(msg.bytes));
+ } else if (msg.type === "stderr") {
const text = new TextDecoder().decode(new Uint8Array(msg.bytes));
- this._emitStatus('[stderr] ' + text);
- } else if (msg.type === 'status') {
+ this._emitStatus("[stderr] " + text);
+ } else if (msg.type === "status") {
this._emitStatus(msg.text);
+ } else if (msg.type === "shared-info") {
+ this.sharedState = new Int32Array(
+ msg.memory,
+ msg.statePtr,
+ 1 + 256 + 256,
+ );
+ console.log("[main] shared-info received, direct futex wake enabled");
+ } else if (msg.type === "ready") {
+ console.log("[main] worker signaled ready, RPC channel should be live");
+ this._readyHandlers.forEach((h) => h());
}
}
_emitStatus(text) {
- console.log('[main] status:', text);
- this._statusHandlers.forEach(h => h(text));
+ console.log("[main] status:", text);
+ this._statusHandlers.forEach((h) => h(text));
}
send(bytes) {
- console.log("[main] SEND: writing", bytes.length, "bytes");
-
-// This ensures all data is in the buffer before Nvim wakes up
-const hexStr = Array.from(bytes)
- .map(b => b.toString(16).padStart(2, '0'))
- .join(' ');
+ console.log("[main] Send: writing", bytes.length, "bytes");
+ const hexStr = Array.from(bytes)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join(" ");
console.log(`[main] Send: ${bytes.length} bytes: ${hexStr}`);
-
+
+ let lastHead = 0;
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i];
const head = Atomics.load(this.state, 0);
const tail = Atomics.load(this.state, 1);
-
const next = (head + 1) % this.CAP;
if (next === tail) {
- console.error('[main] Send: ring buffer full');
+ console.error("[main] Send: ring buffer full");
return false;
}
-
this.ringData[head] = b;
Atomics.store(this.state, 0, next);
- // wait until all bytes are written
+ lastHead = next;
}
-
- // notify once after all bytes are in the buffer
- Atomics.notify(this.state, 0);
-
- console.log("[main] SEND DONE, notified worker");
+
+ if (this.sharedState) {
+ // Write straight into the C-side shared struct, then wake the futex.
+ // This only works if the worker is already inside
+ // emscripten_futex_wait() at the moment call notify() if it is
+ // not waiting yet, the notify does nothing. Atomics.notify() does not
+ // queue up a wake for some future wait() call as it only wakes threads
+ // that are waiting right now.
+ const STDIN_FD = 9;
+ const tail = Atomics.load(this.state, 1);
+ const n = (lastHead - tail + this.CAP) % this.CAP;
+ Atomics.store(this.sharedState, 1 + STDIN_FD, n); // readable[STDIN_FD] = n
+ Atomics.add(this.sharedState, 0, 1); // generation++
+ Atomics.notify(this.sharedState, 0, 1); // wake the futex_wait, if any
+ console.log("[main] Send Done, direct notified futex, n=", n);
+ }
+ // There's a real startup race here: if this send() happens before the
+ // worker's event loop has reached its first Atomics.wait() (e.g.
+ // auto-attach fires the instant ready arrives), the direct notify
+ // above lands on nobody and is silently wasted. And if the C side poll
+ // loop only reacts to notify events instead of rechecking the shared
+ // value on every scan that missed wake is gone for good and the
+ // request just sits in the ring buffer forever.
+ //
+ // But until the worker actually starts blocking, it is still free to
+ // process its own message queue so this postMessage gets handled
+ // right away, routing through the worker's own markReadable() ->
+ // uv_browser_set_readable ccall path. That makes send() correct
+ // whether or not nvim has reached its poll loop yet instead of
+ // depending on lucky notify timing.
+ this.worker.postMessage({ type: "readable-hint" });
+ console.log(
+ "[main] SEND DONE, also sent readable-hint as belt-and-suspenders",
+ );
+
return true;
}
- onBytes(cb) { this._bytesHandlers.push(cb); }
- onStatus(cb) { this._statusHandlers.push(cb); }
- persist() { this.worker.postMessage({ type: 'persist' }); }
+ onBytes(cb) {
+ this._bytesHandlers.push(cb);
+ }
+ onStatus(cb) {
+ this._statusHandlers.push(cb);
+ }
+ onReady(cb) {
+ this._readyHandlers.push(cb);
+ }
+ persist() {
+ this.worker.postMessage({ type: "persist" });
+ }
}
class RpcClient {
@@ -86,98 +135,103 @@ class RpcClient {
this.notificationHandlers = new Map();
this._buffer = new Uint8Array(0);
- this._decodedMessages = [];
-
- transport.onBytes(bytes => this._handleBytes(bytes));
+
+ transport.onBytes((bytes) => this._handleBytes(bytes));
}
_handleBytes(newBytes) {
+ console.log(
+ "[RpcClient] _handleBytes",
+ newBytes.length,
+ newBytes.slice(0, 32),
+ );
+
const combined = new Uint8Array(this._buffer.length + newBytes.length);
combined.set(this._buffer, 0);
combined.set(newBytes, this._buffer.length);
this._buffer = combined;
- const decoder = new MessagePack.Decoder();
- let offset = 0;
+ const { messages, consumed } = MsgpackCodec.decodeMultiple(this._buffer);
- while (offset < this._buffer.length) {
- let msg;
- try {
- // decodes exactly one value starting at `offset` tells us how much it consumed
- decoder.setBuffer(this._buffer.subarray(offset));
- msg = decoder.decode();
- } catch (e) {
- break; // incomplete trailing message — stop, wait for more bytes
- }
- offset += decoder.bytesConsumed ?? decoder.pos; // depends on library version
+ for (const msg of messages) {
+ console.log("[RpcClient] raw decoded:", JSON.stringify(msg));
this._dispatch(msg);
}
- // only drop what was actually consumed and keep any incomplete tail
- this._buffer = this._buffer.slice(offset);
+ this._buffer = this._buffer.slice(consumed);
}
_dispatch(rawMsg) {
- console.log('[RpcClient] dispatching:', rawMsg);
-
+ console.log("[RpcClient] dispatching:", rawMsg);
+
let msg;
try {
msg = Protocol.parseMessage(rawMsg);
} catch (e) {
- console.error('[RpcClient] parse failed', rawMsg, e);
+ console.error("[RpcClient] parse failed", rawMsg, e);
return;
}
- console.log('[RpcClient] parsed:', msg);
+ console.log("[RpcClient] parsed:", msg);
- if (msg.kind === 'response') {
+ if (msg.kind === "response") {
const p = this.pending.get(msg.msgid);
if (!p) {
- console.warn('[RpcClient] unknown msgid', msg.msgid);
+ console.warn("[RpcClient] unknown msgid", msg.msgid, "pending:", [
+ ...this.pending.keys(),
+ ]);
return;
}
this.pending.delete(msg.msgid);
clearTimeout(p.timeoutId);
- console.log('[RpcClient] resolved msgid', msg.msgid);
+ console.log("[RpcClient] resolved msgid", msg.msgid);
if (msg.error) {
p.reject(new Error(msg.error));
} else {
p.resolve(msg.result);
}
- } else if (msg.kind === 'notification') {
- console.log('[RpcClient] notification:', msg.method);
+ } else if (msg.kind === "notification") {
+ console.log("[RpcClient] notification:", msg.method);
const handlers = this.notificationHandlers.get(msg.method) || [];
- handlers.forEach(h => h(msg.params));
- } else if (msg.kind === 'request') {
- console.warn('[RpcClient] unhandled request', msg);
+ handlers.forEach((h) => h(msg.params));
+ } else if (msg.kind === "request") {
+ console.warn("[RpcClient] unhandled request", msg);
}
}
request(method, params = []) {
const msgid = this.nextMsgId++;
const bytes = Protocol.encodeRequest(msgid, method, params);
- console.log(`[RpcClient] sending request ${msgid} (${method}), ${bytes.length} bytes`);
-
+ console.log(
+ `[RpcClient] sending request ${msgid} (${method}), ${bytes.length} bytes`,
+ );
+
return new Promise((resolve, reject) => {
- this.pending.set(msgid, { resolve, reject, method, timestamp: Date.now() });
-
+ this.pending.set(msgid, {
+ resolve,
+ reject,
+ method,
+ timestamp: Date.now(),
+ });
+
const timeoutId = setTimeout(() => {
if (this.pending.has(msgid)) {
this.pending.delete(msgid);
reject(new Error(`Request ${msgid} (${method}) timed out after 30s`));
}
}, 30000);
-
+
this.pending.get(msgid).timeoutId = timeoutId;
-
- // This batches all bytes before notifying
+
this.transport.send(bytes);
});
}
notify(method, params = []) {
const bytes = Protocol.encodeNotification(method, params);
- console.log(`[RpcClient] sending notification ${method}, ${bytes.length} bytes`);
+ console.log(
+ `[RpcClient] sending notification ${method}, ${bytes.length} bytes`,
+ );
this.transport.send(bytes);
}
diff --git a/src/wasm/serve.py b/src/wasm/serve.py
index 753935f9e7..bff568c96b 100644
--- a/src/wasm/serve.py
+++ b/src/wasm/serve.py
@@ -1,8 +1,18 @@
#!/usr/bin/env python3
+
+"""
+Local server for the WASM demo.
+Usage: python3 src/wasm/serve.py
+"""
+
import http.server
import socketserver
+from pathlib import Path
+from functools import partial
-PORT = 8001
+PORT = 8002
+
+ROOT = Path(__file__).resolve().parents[2]
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
@@ -11,6 +21,8 @@ class Handler(http.server.SimpleHTTPRequestHandler):
super().end_headers()
if __name__ == "__main__":
- with socketserver.TCPServer(("", PORT), Handler) as httpd:
- print(f"Serving with COOP/COEP on http://localhost:{PORT}")
+ handler = partial(Handler, directory=str(ROOT))
+
+ with socketserver.TCPServer(("", PORT), handler) as httpd:
+ print(f"Serving {ROOT} on http://localhost:{PORT}")
httpd.serve_forever()
diff --git a/src/wasm/style.css b/src/wasm/style.css
index d27d30a75a..640b80682a 100644
--- a/src/wasm/style.css
+++ b/src/wasm/style.css
@@ -1,5 +1,16 @@
-body { font-family: monospace; background: #1e1e1e; color: #ddd; padding: 10px; margin: 0; }
-#bar { margin-bottom: 10px; }
-#bar button { margin-right: 8px; }
-#status { color: #9c9; }
-#log { white-space: pre-wrap; background: #111; padding: 10px; border-radius: 4px; }
+body {
+ font-family: monospace;
+ background: #1e1e1e;
+ color: #ddd;
+ padding: 10px;
+ margin: 0;
+}
+#bar {
+ margin-bottom: 10px;
+}
+#bar button {
+ margin-right: 8px;
+}
+#status {
+ color: #9c9;
+}
diff --git a/src/wasm_stubs.c b/src/wasm/wasm_stubs.c
similarity index 95%
rename from src/wasm_stubs.c
rename to src/wasm/wasm_stubs.c
index 830121d23e..a83bf6d66a 100644
--- a/src/wasm_stubs.c
+++ b/src/wasm/wasm_stubs.c
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -12,19 +13,6 @@
#include
#include
-#ifndef POLLIN
-# define POLLIN 0x001
-#endif
-#ifndef POLLOUT
-# define POLLOUT 0x004
-#endif
-#ifndef POLLERR
-# define POLLERR 0x008
-#endif
-#ifndef POLLHUP
-# define POLLHUP 0x010
-#endif
-
void uv__io_cb(uv_loop_t *loop, uv__io_t *w, unsigned events);
// System info stubs
@@ -87,7 +75,7 @@ int uv_exepath(char *buffer, size_t *size)
}
// Browsers have no native CPU information. So return a single virtual CPU
-int uv_cpu_info(uv_cpu_info_t **cpu_infos, int *count)
+int uv_cpu_info(uv_cpu_info_t * *cpu_infos, int *count)
{
if (!cpu_infos || !count) {
return UV_EINVAL;
@@ -107,7 +95,7 @@ int uv_cpu_info(uv_cpu_info_t **cpu_infos, int *count)
}
// Browsers do not expose host network interfaces. Report only loopback.
-int uv_interface_addresses(uv_interface_address_t **addresses, int *count)
+int uv_interface_addresses(uv_interface_address_t * *addresses, int *count)
{
if (!addresses || !count) {
return UV_EINVAL;