mirror of
https://github.com/neovim/neovim.git
synced 2026-08-14 03:04:54 +00:00
Merge pull request #40772 from Rawan10101/wasm-browser
feat(wasm-emcc): add browser support for Neovim
This commit is contained in:
10
build.zig
10
build.zig
@@ -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}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
393
src/wasm/app.js
Normal 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 "&";
|
||||
if (ch === "<") return "<";
|
||||
if (ch === ">") return ">";
|
||||
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
53
src/wasm/index.html
Normal 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
62
src/wasm/msgpack.js
Normal 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
34
src/wasm/msgpackrpc.js
Normal 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
597
src/wasm/nvim-worker.js
Normal 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
245
src/wasm/rpc.js
Normal 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
28
src/wasm/serve.py
Normal 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
16
src/wasm/style.css
Normal 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
314
src/wasm/wasm_stubs.c
Normal 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;
|
||||
}
|
||||
181
src/wasm_stubs.c
181
src/wasm_stubs.c
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user