Merge pull request #40772 from Rawan10101/wasm-browser

feat(wasm-emcc): add browser support for Neovim
This commit is contained in:
bfredl
2026-08-04 12:11:17 +02:00
committed by GitHub
14 changed files with 1757 additions and 189 deletions

View File

@@ -590,7 +590,7 @@ pub fn build(b: *std.Build) !void {
if (is_wasm) {
nvim_mod.addCSourceFiles(.{ .files = &.{
"src/wasm_stubs.c",
"src/wasm/wasm_stubs.c",
"src/static_ts_registry.c",
}, .flags = &flags });
}
@@ -637,14 +637,12 @@ pub fn build(b: *std.Build) !void {
emcc.addArgs(&.{
b.fmt("--sysroot={s}", .{s}),
"-lidbfs.js",
"-sALLOW_MEMORY_GROWTH=1",
"--profiling-funcs",
"-sEXPORTED_FUNCTIONS=_nvim_main,_malloc,_free",
"-Wno-undefined",
"-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,ENV,FS,HEAPU8,IDBFS",
"-sEXPORTED_RUNTIME_METHODS=stringToUTF8,lengthBytesUTF8,setValue,getValue,UTF8ToString,ENV,FS,HEAPU8,IDBFS,ccall,cwrap",
"-sSTACK_SIZE=8388608",
"-sINITIAL_MEMORY=268435456",
"-sMAXIMUM_MEMORY=2147483648",
"-sFORCE_FILESYSTEM=1",
"-sNODERAWFS=0",
"-sERROR_ON_UNDEFINED_SYMBOLS=1",
@@ -657,8 +655,8 @@ pub fn build(b: *std.Build) !void {
"-flto",
"-sSTACK_OVERFLOW_CHECK=2",
"-sASSERTIONS=1",
"-sASYNCIFY=1",
"-sASYNCIFY_STACK_SIZE=65536",
"-pthread",
"-sSHARED_MEMORY=1",
b.fmt("--preload-file={s}@/runtime", .{merged_path}),
});

View File

@@ -220,6 +220,10 @@ BUILD
(empty string) to assume that target binaries can run on the host during the
build process (e.g. if target is x86 on a x86_64 system, or if emulation set
up with binfmt or similar).
• zig build: Nvim can now be built to produce a WebAssembly binary via
"-Dtarget=wasm32-emscripten", which can run inside a web browser. A basic
browser-based demo for running it is also included. Still very
experimental.
DEFAULTS

View File

@@ -445,7 +445,7 @@ list(APPEND UNCRUSTIFY_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS})
list(APPEND UNCRUSTIFY_NVIM_SOURCES
${PROJECT_SOURCE_DIR}/src/tee/tee.c
${PROJECT_SOURCE_DIR}/src/xxd/xxd.c
${PROJECT_SOURCE_DIR}/src/wasm_stubs.c
${PROJECT_SOURCE_DIR}/src/wasm/wasm_stubs.c
${PROJECT_SOURCE_DIR}/src/static_ts_registry.c
)

View File

@@ -80,8 +80,13 @@ int wstream_write(Stream *stream, WBuffer *buffer)
uv_fs_t req;
// Synchronous write
err = uv_fs_write(stream->uv.idle.loop, &req, stream->fd, &uvbuf, 1, stream->fpos, NULL);
#ifdef __EMSCRIPTEN__
// Pass -1 so the write uses the current file position.
err = uv_fs_write(stream->uv.idle.loop, &req, stream->fd, &uvbuf, 1, -1, NULL);
#else
err = uv_fs_write(stream->uv.idle.loop, &req, stream->fd, &uvbuf, 1, stream->fpos, NULL);
#endif
uv_fs_req_cleanup(&req);
wstream_release_wbuffer(buffer);

393
src/wasm/app.js Normal file
View File

@@ -0,0 +1,393 @@
const DEBUG = false;
const statusEl = document.getElementById("status");
const setStatus = (s) => (statusEl.textContent = s);
const log = (...args) => {
if (!DEBUG) return;
if (logEl) {
logEl.textContent +=
args
.map((a) => (typeof a === "string" ? a : JSON.stringify(a)))
.join(" ") + "\n";
}
console.log(...args);
};
// UiState below is ported from (github.com/MuNeNiCK/nvim-wasm)
// but, extended with win_viewport tracking.
class UiState {
constructor(cols, rows) {
this.defaultWidth = Math.max(1, cols || 80);
this.defaultHeight = Math.max(1, rows || 24);
this.defaultGrid = 1;
this.activeGrid = this.defaultGrid;
this.grids = new Map();
this.grids.set(
this.defaultGrid,
this.#createGrid(this.defaultWidth, this.defaultHeight),
);
this.cursor = { grid: this.defaultGrid, row: 0, col: 0 };
this.mode = "-";
this.modeIdx = 0;
this.cursorStyleEnabled = false;
this.modeInfo = [];
this.cursorHlId = 0;
this.hls = new Map();
this.hls.set(0, { foreground: null, background: null, reverse: false });
this.windows = new Map();
}
resize(gridId, width, height) {
const grid = this.#ensureGrid(gridId);
const w = Math.max(1, width || 0);
const h = Math.max(1, height || 0);
grid.width = w;
grid.height = h;
grid.cells = Array.from({ length: h }, () => this.#blankRow(w));
}
clear(gridId) {
const grid = this.#ensureGrid(gridId);
grid.cells = Array.from({ length: grid.height }, () =>
this.#blankRow(grid.width),
);
}
destroy(gridId) {
this.grids.delete(gridId);
this.windows.delete(gridId);
if (this.activeGrid === gridId) this.activeGrid = this.defaultGrid;
}
line(gridId, row, colStart, cells) {
const grid = this.#ensureGrid(gridId);
const rowIdx = row || 0;
if (rowIdx < 0 || rowIdx >= grid.height) return;
const rowCells =
grid.cells[rowIdx] || (grid.cells[rowIdx] = this.#blankRow(grid.width));
let col = colStart || 0;
let currentHl = 0;
for (const cell of cells) {
const text = cell[0];
const hlId =
cell.length > 1 && cell[1] !== undefined ? cell[1] : currentHl;
currentHl = hlId;
const repeat = cell[2] || 1;
for (let r = 0; r < repeat && col < grid.width; r += 1) {
rowCells[col] = { ch: text || " ", hl: hlId };
col += 1;
}
}
}
scroll(gridId, top, bot, left, right, rows) {
const grid = this.#ensureGrid(gridId);
const height = bot - top;
const width = right - left;
const slice = [];
for (let i = 0; i < height; i += 1) {
const row = grid.cells[top + i] || this.#blankRow(grid.width);
slice.push(row.slice(left, right));
}
if (rows > 0) {
for (let i = 0; i < height - rows; i += 1) {
grid.cells[top + i].splice(left, width, ...slice[i + rows]);
}
for (let i = height - rows; i < height; i += 1) {
grid.cells[top + i].splice(left, width, ...this.#blankRow(width));
}
} else if (rows < 0) {
for (let i = height - 1; i >= -rows; i -= 1) {
grid.cells[top + i].splice(left, width, ...slice[i + rows]);
}
for (let i = 0; i < -rows; i += 1) {
grid.cells[top + i].splice(left, width, ...this.#blankRow(width));
}
}
}
setCursor(gridId, row, col) {
this.activeGrid = gridId;
this.#ensureGrid(gridId);
this.cursor = { grid: gridId, row: row || 0, col: col || 0 };
}
setMode(mode, modeIdx) {
this.mode = mode || "-";
if (Number.isInteger(modeIdx)) this.modeIdx = modeIdx;
this.#updateCursorHl();
}
setModeInfo(cursorStyleEnabled, modeInfo) {
this.cursorStyleEnabled = Boolean(cursorStyleEnabled);
this.modeInfo = Array.isArray(modeInfo) ? modeInfo : [];
this.#updateCursorHl();
}
defineHl(id, rgbAttr = {}) {
this.hls.set(id, {
foreground: this.#toHex(rgbAttr.foreground),
background: this.#toHex(rgbAttr.background),
reverse: Boolean(rgbAttr.reverse),
});
}
setWindowViewport(gridId, winHandle, topline, botline) {
this.windows.set(gridId, { winHandle, topline, botline });
}
snapshot() {
const grid =
this.grids.get(this.activeGrid) || this.grids.get(this.defaultGrid);
if (!grid)
return {
cells: [[{ ch: " ", hl: 0 }]],
cursor: { row: 0, col: 0 },
mode: this.mode,
hls: this.hls,
};
const row = Math.min(Math.max(this.cursor.row, 0), grid.height - 1);
const col = Math.min(Math.max(this.cursor.col, 0), grid.width - 1);
return {
cells: grid.cells.map((r) =>
r.map((c) => ({ ch: c?.ch ?? " ", hl: c?.hl ?? 0 })),
),
cursor: { row, col },
cursorHlId: this.cursorHlId,
mode: this.mode,
hls: this.hls,
};
}
#ensureGrid(gridId) {
if (!this.grids.has(gridId))
this.grids.set(
gridId,
this.#createGrid(this.defaultWidth, this.defaultHeight),
);
return this.grids.get(gridId);
}
#createGrid(width, height) {
return {
width,
height,
cells: Array.from({ length: height }, () => this.#blankRow(width)),
};
}
#blankRow(width) {
return Array.from({ length: width }, () => ({ ch: " ", hl: 0 }));
}
#updateCursorHl() {
if (!this.cursorStyleEnabled) {
this.cursorHlId = 0;
return;
}
const info = this.modeInfo[this.modeIdx] || null;
this.cursorHlId = info?.attr_id ?? 0;
}
#toHex(value) {
if (value === undefined || value === null) return null;
return `#${(value >>> 0).toString(16).padStart(6, "0").slice(-6)}`;
}
}
function handleRedrawEvents(uiState, gridEl, modeEl, events) {
for (const ev of events) {
const [name, ...entries] = ev;
switch (name) {
case "grid_resize":
for (const [grid, w, h] of entries) uiState.resize(grid, w, h);
break;
case "grid_clear":
for (const [grid] of entries) uiState.clear(grid);
break;
case "grid_destroy":
for (const [grid] of entries) uiState.destroy(grid);
break;
case "grid_line":
for (const [grid, row, col, cells] of entries)
uiState.line(grid, row, col, cells);
break;
case "grid_scroll":
for (const [grid, top, bot, left, right, rows] of entries)
uiState.scroll(grid, top, bot, left, right, rows);
break;
case "grid_cursor_goto":
for (const [grid, row, col] of entries)
uiState.setCursor(grid, row, col);
break;
case "win_viewport":
for (const [grid, win, topline, botline] of entries)
uiState.setWindowViewport(grid, win, topline, botline);
break;
case "mode_info_set":
for (const [enabled, modeInfo] of entries)
uiState.setModeInfo(enabled, modeInfo);
break;
case "mode_change":
for (const [mode, idx] of entries) uiState.setMode(mode, idx);
break;
case "hl_attr_define":
for (const [id, rgbAttr] of entries) uiState.defineHl(id, rgbAttr);
break;
case "flush":
paintGrid(uiState, gridEl, modeEl);
break;
default:
break; // mouse on/off, busy start/stop, etc.
}
}
}
function paintGrid(uiState, gridEl, modeEl) {
const { cells, cursor, cursorHlId, mode, hls } = uiState.snapshot();
let html = "";
for (let r = 0; r < cells.length; r += 1) {
for (let c = 0; c < cells[r].length; c += 1) {
const cell = cells[r][c];
const isCursor = r === cursor.row && c === cursor.col;
const hl = hls.get(isCursor ? cursorHlId : cell.hl) || {};
let style = "";
if (hl.reverse) {
style = `color:${hl.background || "#050912"};background:${hl.foreground || "#dfe5f1"}`;
} else {
if (hl.foreground) style += `color:${hl.foreground};`;
if (hl.background) style += `background:${hl.background};`;
}
const ch = escapeHtml(cell.ch || " ");
if (isCursor) {
html += `<span class="cursor" style="${style}">${ch}</span>`;
} else if (style) {
html += `<span style="${style}">${ch}</span>`;
} else {
html += ch;
}
}
if (r < cells.length - 1) html += "\n";
}
gridEl.innerHTML = html || " ";
if (modeEl) modeEl.textContent = `mode: ${mode}`;
}
function escapeHtml(ch) {
if (ch === "&") return "&amp;";
if (ch === "<") return "&lt;";
if (ch === ">") return "&gt;";
return ch;
}
function translateKey(ev) {
const isCtrl = ev.ctrlKey || ev.metaKey;
const isAlt = ev.altKey;
const named = {
Backspace: "<BS>",
Enter: "<CR>",
Escape: "<Esc>",
Tab: "<Tab>",
ArrowUp: "<Up>",
ArrowDown: "<Down>",
ArrowLeft: "<Left>",
ArrowRight: "<Right>",
Delete: "<Del>",
Home: "<Home>",
End: "<End>",
PageUp: "<PageUp>",
PageDown: "<PageDown>",
};
if (named[ev.key]) return named[ev.key];
if (ev.key.length === 1) {
let char = ev.shiftKey ? ev.key : ev.key.toLowerCase();
if (char === "<") char = "<lt>";
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: 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);
window.nvim = nvim; // for console debugging
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;
})();
}
return apiInfoPromise;
}
// 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));
}
}
}
});
nvim.on("redraw", (params) =>
handleRedrawEvents(uiState, gridEl, modeEl, params),
);
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();
}

53
src/wasm/index.html Normal file
View File

@@ -0,0 +1,53 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>nvim.wasm RPC client</title>
<link rel="stylesheet" href="style.css" />
<style>
#grid {
font-family: "SFMono-Regular", Menlo, Consolas, monospace;
font-size: 14px;
line-height: 1.35;
white-space: pre;
background: #050912;
color: #dfe5f1;
border: 1px solid #2a3346;
border-radius: 8px;
padding: 8px;
outline: none;
overflow: auto;
}
#grid:focus {
border-color: #5ad1ff;
}
.cursor {
outline: 1px solid currentColor;
}
#bar {
display: none;
}
</style>
</head>
<body>
<div id="grid" tabindex="0" aria-label="Neovim grid"></div>
<div id="bar">
<span id="status">booting…</span>
</div>
<script
crossorigin="anonymous"
src="https://cdn.jsdelivr.net/npm/msgpackr/dist/index.js"
></script>
<script src="msgpack.js"></script>
<script src="msgpackrpc.js"></script>
<script src="rpc.js"></script>
<script src="app.js"></script>
</body>
</html>

62
src/wasm/msgpack.js Normal file
View File

@@ -0,0 +1,62 @@
if (typeof msgpackr === 'undefined') {
throw new Error(
'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 = 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;
},
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 || '');
}

34
src/wasm/msgpackrpc.js Normal file
View File

@@ -0,0 +1,34 @@
// msgpack-rpc message shapes:
// request: [0, msgid, method, params]
// response: [1, msgid, error, result]
// notification: [2, method, params]
const Protocol = {
encodeRequest(msgid, method, params) {
return MsgpackCodec.encode([0, msgid, method, params]);
},
encodeNotification(method, params) {
return MsgpackCodec.encode([2, method, params]);
},
// classifies a decoded msgpack-rpc array into a tagged object.
// 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");
const [type, ...rest] = msg;
if (type === 0) {
const [msgid, method, params] = rest;
return { kind: "request", msgid, method, params };
}
if (type === 1) {
const [msgid, error, result] = rest;
return { kind: "response", msgid, error, result };
}
if (type === 2) {
const [method, params] = rest;
return { kind: "notification", method, params };
}
throw new Error("unknown rpc message type: " + type);
},
};

597
src/wasm/nvim-worker.js Normal file
View File

@@ -0,0 +1,597 @@
const CAP = 1 << 16;
let state = null;
let ringData = null;
let moduleRef = null;
let totalBytesRead = 0;
const consumedBytes = [];
const STATUS_MSG_CAP = 2000;
let statusMsgCount = 0;
let statusCapNoticeSent = false;
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 });
}
const perFdWriteCount = {};
function bumpFdWriteCount(fd) {
perFdWriteCount[fd] = (perFdWriteCount[fd] || 0) + 1;
}
/* File descriptor monitored by libuv for stdin. Verify this value
against the startup FD dump, as it may differ across environments. */
const STDIN_FD = 9;
/*
SharedArrayBuffer layout:
offset 0:
Int32Array:
[0] head
[1] tail
[2] closed
offset 12:
Uint8Array data
*/
function unreadCount() {
const head = Atomics.load(state, 0);
const tail = Atomics.load(state, 1);
return (head - tail + CAP) % CAP;
}
// 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);
}
}
// 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() {
if (stdoutBuffer.length) {
postMessage({
type: "stdout",
bytes: stdoutBuffer,
});
stdoutBuffer = [];
}
if (stderrBuffer.length) {
postMessage({
type: "stderr",
bytes: stderrBuffer,
});
stderrBuffer = [];
}
}
let stdoutBuffer = [];
let stderrBuffer = [];
function writeStderr(c) {
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;
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,
};
}
self.onerror = (e) => {
safeStatus("WORKER ERROR " + e.message);
};
self.onunhandledrejection = (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);
};
/* 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,
);
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 (Atomics.load(state, 2) === 1) {
console.log("stdin closed");
return null; // real EOF
}
return undefined; // no data yet -> Emscripten turns this into EAGAIN
};
}
self.onmessage = async (ev) => {
const msg = ev.data;
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
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,
noInitialRun: true,
interactive: false,
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;
attr.mode = attr.mode ?? 0o666;
attr.nlink = attr.nlink ?? 1;
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.blksize = attr.blksize ?? 4096;
attr.blocks = attr.blocks ?? 0;
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);
return attr;
}
function isStdinLike(path) {
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;
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];
if (isStdinLike(path)) {
attr.mode = 0o010666;
}
return sanitizeAttr(attr);
};
});
const origFSWrite = m.FS.write;
m.FS.write = function (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", "--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();
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)`
: "";
safeStatus(
`EXIT CODE ${ret}. Read ${totalBytesRead} bytes: ${hexStr}${remaining}`,
);
}
if (msg.type === "stdin") {
pushBytes(msg.bytes);
}
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 anything Atomics.wait()ing
markReadable(); // also wake the C side futex so uv__io_poll rescans and sees the close
}
}
};

245
src/wasm/rpc.js Normal file
View File

@@ -0,0 +1,245 @@
// TODO: Add a fallback when SharedArrayBuffer is unavailable.
// Transport: owns the worker and 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.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);
this._onWorkerMessage(ev.data);
};
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" });
}
_onWorkerMessage(msg) {
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") {
const text = new TextDecoder().decode(new Uint8Array(msg.bytes));
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));
}
send(bytes) {
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");
return false;
}
this.ringData[head] = b;
Atomics.store(this.state, 0, next);
lastHead = next;
}
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);
}
onReady(cb) {
this._readyHandlers.push(cb);
}
persist() {
this.worker.postMessage({ type: "persist" });
}
}
class RpcClient {
constructor(transport) {
this.transport = transport;
this.nextMsgId = 1;
this.pending = new Map();
this.notificationHandlers = new Map();
this._buffer = new Uint8Array(0);
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 { messages, consumed } = MsgpackCodec.decodeMultiple(this._buffer);
for (const msg of messages) {
console.log("[RpcClient] raw decoded:", JSON.stringify(msg));
this._dispatch(msg);
}
this._buffer = this._buffer.slice(consumed);
}
_dispatch(rawMsg) {
console.log("[RpcClient] dispatching:", rawMsg);
let msg;
try {
msg = Protocol.parseMessage(rawMsg);
} catch (e) {
console.error("[RpcClient] parse failed", rawMsg, e);
return;
}
console.log("[RpcClient] parsed:", msg);
if (msg.kind === "response") {
const p = this.pending.get(msg.msgid);
if (!p) {
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);
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);
const handlers = this.notificationHandlers.get(msg.method) || [];
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`,
);
return new Promise((resolve, reject) => {
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.transport.send(bytes);
});
}
notify(method, params = []) {
const bytes = Protocol.encodeNotification(method, params);
console.log(
`[RpcClient] sending notification ${method}, ${bytes.length} bytes`,
);
this.transport.send(bytes);
}
on(method, handler) {
if (!this.notificationHandlers.has(method)) {
this.notificationHandlers.set(method, []);
}
this.notificationHandlers.get(method).push(handler);
}
}

28
src/wasm/serve.py Normal file
View File

@@ -0,0 +1,28 @@
#!/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 = 8002
ROOT = Path(__file__).resolve().parents[2]
class Handler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
super().end_headers()
if __name__ == "__main__":
handler = partial(Handler, directory=str(ROOT))
with socketserver.TCPServer(("", PORT), handler) as httpd:
print(f"Serving {ROOT} on http://localhost:{PORT}")
httpd.serve_forever()

16
src/wasm/style.css Normal file
View File

@@ -0,0 +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;
}

314
src/wasm/wasm_stubs.c Normal file
View File

@@ -0,0 +1,314 @@
#include <emscripten.h>
#include <emscripten/threading.h>
#include <math.h>
#include <poll.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <uv.h>
void uv__io_cb(uv_loop_t *loop, uv__io_t *w, unsigned events);
// System info stubs
uint64_t uv_get_free_memory(void)
{
return 8 * 1024 * 1024;
}
uint64_t uv_get_total_memory(void)
{
return 256 * 1024 * 1024;
}
uint64_t uv_get_available_memory(void)
{
return uv_get_free_memory();
}
uint64_t uv_get_constrained_memory(void)
{
return uv_get_total_memory();
}
void uv_loadavg(double avg[3])
{
avg[0] = 0.0; avg[1] = 0.0; avg[2] = 0.0;
}
int uv_uptime(double *uptime)
{
if (!uptime) {
return UV_EINVAL;
}
*uptime = emscripten_get_now() / 1000.0;
return 0;
}
int uv_resident_set_memory(size_t *rss)
{
if (!rss) {
return UV_EINVAL;
}
*rss = uv_get_total_memory() / 2; // fake value
return 0;
}
int uv_exepath(char *buffer, size_t *size)
{
if (!buffer || !size) {
return UV_EINVAL;
}
const char *exepath = "/nvim.wasm";
size_t len = strlen(exepath);
if (*size <= len) {
*size = len + 1;
return UV_ENOBUFS;
}
memcpy(buffer, exepath, len);
buffer[len] = '\0';
*size = len;
return 0;
}
// Browsers have no native CPU information. So return a single virtual CPU
int uv_cpu_info(uv_cpu_info_t * *cpu_infos, int *count)
{
if (!cpu_infos || !count) {
return UV_EINVAL;
}
*cpu_infos = (uv_cpu_info_t *)malloc(sizeof(uv_cpu_info_t));
if (!*cpu_infos) {
return UV_ENOMEM;
}
uv_cpu_info_t *cpu = *cpu_infos;
cpu->model = "WebAssembly Virtual CPU";
cpu->speed = 0;
cpu->cpu_times.user = 0; cpu->cpu_times.nice = 0; cpu->cpu_times.sys = 0;
cpu->cpu_times.idle = 0; cpu->cpu_times.irq = 0;
*count = 1;
return 0;
}
// Browsers do not expose host network interfaces. Report only loopback.
int uv_interface_addresses(uv_interface_address_t * *addresses, int *count)
{
if (!addresses || !count) {
return UV_EINVAL;
}
*addresses = (uv_interface_address_t *)malloc(sizeof(uv_interface_address_t));
if (!*addresses) {
return UV_ENOMEM;
}
uv_interface_address_t *addr = *addresses;
memset(addr, 0, sizeof(uv_interface_address_t));
addr->name = strdup("lo");
if (!addr->name) {
free(addr); return UV_ENOMEM;
}
addr->is_internal = 1;
uv_ip4_addr("127.0.0.1", 0, (struct sockaddr_in *)&addr->address);
uv_ip4_addr("255.0.0.0", 0, (struct sockaddr_in *)&addr->netmask);
*count = 1;
return 0;
}
#define UV_BROWSER_MAX_FD 256
typedef struct {
_Atomic uint32_t generation; // woken on any fd state change
_Atomic int32_t readable[UV_BROWSER_MAX_FD];
_Atomic int32_t writable[UV_BROWSER_MAX_FD];
} uv_browser_shared_state_t;
static uv_browser_shared_state_t g_shared_state;
EMSCRIPTEN_KEEPALIVE
void *uv_browser_get_shared_state_ptr(void)
{
return (void *)&g_shared_state;
}
EMSCRIPTEN_KEEPALIVE
void uv_browser_set_readable(int fd, int32_t nbytes)
{
if (fd < 0 || fd >= UV_BROWSER_MAX_FD) {
return;
}
atomic_store(&g_shared_state.readable[fd], nbytes);
atomic_fetch_add(&g_shared_state.generation, 1);
emscripten_futex_wake(&g_shared_state.generation, 1);
}
EMSCRIPTEN_KEEPALIVE
void uv_browser_set_writable(int fd, int32_t nbytes)
{
if (fd < 0 || fd >= UV_BROWSER_MAX_FD) {
return;
}
atomic_store(&g_shared_state.writable[fd], nbytes);
atomic_fetch_add(&g_shared_state.generation, 1);
emscripten_futex_wake(&g_shared_state.generation, 1);
}
static int uv_browser_scan_ready(uv_loop_t *loop, unsigned *out_idx, int *out_revents)
{
unsigned i;
uv__io_t *w;
int revents;
for (i = 0; i < loop->nwatchers && i < UV_BROWSER_MAX_FD; i++) {
w = loop->watchers[i];
if (w == NULL || w->pevents == 0) {
continue;
}
revents = 0;
if ((w->pevents & POLLIN) && atomic_load(&g_shared_state.readable[i]) > 0) {
revents |= POLLIN;
}
if ((w->pevents & POLLOUT) && atomic_load(&g_shared_state.writable[i]) > 0) {
revents |= POLLOUT;
}
if (revents) {
*out_idx = i;
*out_revents = revents;
return 1;
}
}
return 0;
}
void uv__io_poll(uv_loop_t *loop, int timeout)
{
double deadline;
double remaining;
uint32_t g0;
unsigned idx;
int revents;
int have_signal;
uv__io_t *w;
deadline = (timeout < 0) ? -1.0 : (emscripten_get_now() + (double)timeout);
have_signal = 0;
while (true) {
/* Any state change that lands between this load and the scan below is still caught by the scan
itself. Any change that lands after the scan will bump generation past g0, so the futex_wait call
falls through immediately instead of sleeping through it. This closes the narrow window where a notify
could otherwise land between "nothing is ready" and "waiting". */
g0 = atomic_load(&g_shared_state.generation);
if (uv_browser_scan_ready(loop, &idx, &revents)) {
have_signal = 1;
break;
}
if (deadline >= 0.0) {
remaining = deadline - emscripten_get_now();
if (remaining <= 0.0) {
break;
}
} else {
remaining = 60000.0;
}
int rc = emscripten_futex_wait(&g_shared_state.generation, g0, remaining);
}
uv_update_time(loop);
if (!have_signal) {
return;
}
for (idx = 0; idx < loop->nwatchers && idx < UV_BROWSER_MAX_FD; idx++) {
w = loop->watchers[idx];
if (w == NULL || w->pevents == 0) {
continue;
}
revents = 0;
if ((w->pevents & POLLIN) && atomic_load(&g_shared_state.readable[idx]) > 0) {
revents |= POLLIN;
}
if ((w->pevents & POLLOUT) && atomic_load(&g_shared_state.writable[idx]) > 0) {
revents |= POLLOUT;
}
revents &= w->pevents | POLLERR | POLLHUP;
if (revents == 0) {
continue;
}
uv__io_cb(loop, w, revents);
}
}
int uv__platform_loop_init(uv_loop_t *loop)
{
if (!loop) {
return UV_EINVAL;
}
loop->backend_fd = -1;
return 0;
}
void uv__platform_loop_delete(uv_loop_t *loop)
{
(void)loop;
}
void uv__platform_invalidate_fd(uv_loop_t *loop, int fd)
{
if (fd < 0 || fd >= UV_BROWSER_MAX_FD) {
return;
}
atomic_store(&g_shared_state.readable[fd], 0);
atomic_store(&g_shared_state.writable[fd], 0);
}
int uv__io_check_fd(uv_loop_t *loop, int fd)
{
(void)loop; (void)fd;
return 0;
}
int uv__io_fork(uv_loop_t *loop)
{
(void)loop;
return 0;
}
// pthread shims
int pthread_getname_np(pthread_t thread, char *name, size_t len)
{
(void)thread;
if (!name || len < 1) {
return EINVAL;
}
strncpy(name, "nvim-main", len - 1);
name[len - 1] = '\0';
return 0;
}
int pthread_setname_np(pthread_t thread, const char *name)
{
(void)thread; (void)name;
return 0;
}
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param)
{
(void)thread; (void)policy; (void)param;
return 0;
}

View File

@@ -1,181 +0,0 @@
#include <emscripten.h>
#include <pthread.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <uv.h>
uint64_t uv_get_free_memory(void)
{
return 8 * 1024 * 1024;
}
uint64_t uv_get_total_memory(void)
{
return 256 * 1024 * 1024;
}
uint64_t uv_get_available_memory(void)
{
return uv_get_free_memory();
}
uint64_t uv_get_constrained_memory(void)
{
return uv_get_total_memory();
}
// Report an idle virtual system
void uv_loadavg(double avg[3])
{
avg[0] = 0.0; avg[1] = 0.0; avg[2] = 0.0;
}
int uv_uptime(double *uptime)
{
if (!uptime) {
return UV_EINVAL;
}
*uptime = emscripten_get_now() / 1000.0;
return 0;
}
int uv_resident_set_memory(size_t *rss)
{
if (!rss) {
return UV_EINVAL;
}
*rss = uv_get_total_memory() / 2;
return 0;
}
int uv_exepath(char *buffer, size_t *size)
{
if (!buffer || !size) {
return UV_EINVAL;
}
const char *exepath = "/nvim.wasm";
size_t len = strlen(exepath);
if (*size <= len) {
*size = len + 1;
return UV_ENOBUFS;
}
memcpy(buffer, exepath, len);
buffer[len] = '\0';
*size = len;
return 0;
}
int uv__io_fork(uv_loop_t *loop)
{
(void)loop;
return 0;
}
int uv_cpu_info(uv_cpu_info_t * *cpu_infos, int *count)
{
if (!cpu_infos || !count) {
return UV_EINVAL;
}
*cpu_infos = (uv_cpu_info_t *)malloc(sizeof(uv_cpu_info_t));
if (!*cpu_infos) {
return UV_ENOMEM;
}
uv_cpu_info_t *cpu = *cpu_infos;
cpu->model = "WebAssembly Virtual CPU";
cpu->speed = 0;
cpu->cpu_times.user = 0; cpu->cpu_times.nice = 0; cpu->cpu_times.sys = 0;
cpu->cpu_times.idle = 0; cpu->cpu_times.irq = 0;
*count = 1;
return 0;
}
int uv_interface_addresses(uv_interface_address_t * *addresses, int *count)
{
if (!addresses || !count) {
return UV_EINVAL;
}
*addresses = (uv_interface_address_t *)malloc(sizeof(uv_interface_address_t));
if (!*addresses) {
return UV_ENOMEM;
}
uv_interface_address_t *addr = *addresses;
memset(addr, 0, sizeof(uv_interface_address_t));
addr->name = strdup("lo");
if (!addr->name) {
free(addr); return UV_ENOMEM;
}
addr->is_internal = 1;
uv_ip4_addr("127.0.0.1", 0, (struct sockaddr_in *)&addr->address);
uv_ip4_addr("255.0.0.0", 0, (struct sockaddr_in *)&addr->netmask);
*count = 1;
return 0;
}
void uv__platform_invalidate_fd(uv_loop_t *loop, int fd)
{
(void)loop; (void)fd;
}
int uv__io_check_fd(uv_loop_t *loop, void *w)
{
(void)loop; (void)w; return 0;
}
// Replaces the kernel poll() call
void uv__io_poll(uv_loop_t *loop, int timeout)
{
if (timeout > 0) {
emscripten_sleep(timeout);
uv_update_time(loop);
emscripten_sleep(10);
uv_update_time(loop);
} else if (timeout == 0) {
emscripten_sleep(0);
}
}
int uv__platform_loop_init(uv_loop_t *loop)
{
if (!loop) {
return UV_EINVAL;
}
loop->backend_fd = -1;
return 0;
}
void uv__platform_loop_delete(uv_loop_t *loop)
{
(void)loop;
}
// Fakes thread names when nvim tracks or debugs its internal processes
int pthread_getname_np(pthread_t thread, char *name, size_t len)
{
(void)thread;
if (!name || len < 1) {
return EINVAL;
}
strncpy(name, "nvim-main", len - 1);
name[len - 1] = '\0';
return 0;
}
int pthread_setname_np(pthread_t thread, const char *name)
{
(void)thread; (void)name; return 0;
}
int pthread_setschedparam(pthread_t thread, int policy, const struct sched_param *param)
{
(void)thread; (void)policy; (void)param; return 0;
}
int sched_get_priority_max(int policy)
{
(void)policy; return 1;
}
int sched_get_priority_min(int policy)
{
(void)policy; return 1;
}