Skip to content

Instantly share code, notes, and snippets.

@andy0130tw
Last active August 21, 2025 17:54
Show Gist options
  • Save andy0130tw/9c6bf71ae3bc613e543a1650f9ccb33c to your computer and use it in GitHub Desktop.
Save andy0130tw/9c6bf71ae3bc613e543a1650f9ccb33c to your computer and use it in GitHub Desktop.

Revisions

  1. andy0130tw revised this gist Aug 21, 2025. No changes.
  2. andy0130tw created this gist Aug 21, 2025.
    144 changes: 144 additions & 0 deletions patch-wasi.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,144 @@
    import * as Runno from '@runno/wasi'
    import wrapPollOneoff from './wrap-poll-oneoff'

    const { Result } = Runno.WASISnapshotPreview1

    /** @typedef {{ lenTotal: number, ptrlens: [number, number][] }} IovsDesc */

    /**
    * @param {DataView} view
    * @param {number} iovs_ptr
    * @param {number} iovs_len
    * @returns {IovsDesc}
    */
    function collectIOVectors(view, iovs_ptr, iovs_len) {
    /** @type {[number, number][]} */
    const ptrlens = []
    let lenTotal = 0

    for (let i = 0; i < iovs_len; i++) {
    const bufferPtr = view.getUint32(iovs_ptr, true)
    iovs_ptr += 4

    const bufferLen = view.getUint32(iovs_ptr, true)
    iovs_ptr += 4
    lenTotal += bufferLen

    ptrlens.push([bufferPtr, bufferLen])
    }

    return { lenTotal, ptrlens }
    }

    /**
    * @param {DataView} view
    * @param {IovsDesc} iovsDesc
    * @returns {Uint8Array}
    */
    function readIOVectorsMerged(view, iovsDesc) {
    const source = new Uint8Array(view.buffer, view.byteOffset, view.byteLength)

    const result = new Uint8Array(iovsDesc.lenTotal)
    let written = 0
    for (const [ptr, len] of iovsDesc.ptrlens) {
    // XXX: is there a cleaner way?
    result.set(source.subarray(ptr, ptr + len), written)
    written += len
    }

    return result
    }

    /**
    * @param {Uint8Array} buf
    * @param {IovsDesc} iovsDesc
    * @param {Uint8Array} input
    */
    function writeIntoIOVectors(buf, iovsDesc, input) {
    const { ptrlens } = iovsDesc
    let written = 0
    for (const [ptr, len] of ptrlens) {
    const extent = Math.min(written + len, input.byteLength)
    buf.set(input.slice(written, extent), ptr)
    written = extent
    if (written === input.byteLength) break
    }
    }

    /**
    * @this {Runno.WASI}
    * @param {Runno.WASI['fd_read']} origFdRead
    * @returns {Runno.WASI['fd_read']}
    */
    function wrapFdRead(origFdRead) {
    return (...args) => {
    const [fd, iovs_ptr, iovs_len, retptr0] = args
    if (fd !== 0) return origFdRead(...args)

    const view = new DataView(this.memory.buffer)
    const iovDescs = collectIOVectors(view, iovs_ptr, iovs_len)

    // not knowing a good reason why the original impl. requests
    // one read per iov

    const input = /** @type {Uint8Array | null} */(
    /** @type {unknown} */(this.context.stdin(iovDescs.lenTotal)))

    if (input == null) {
    return Result.EAGAIN
    }

    const bytes = Math.min(iovDescs.lenTotal, input.byteLength)
    writeIntoIOVectors(new Uint8Array(this.memory.buffer), iovDescs, input)

    // FIXME: missing pushDebugData
    view.setUint32(retptr0, bytes, true)
    return Result.SUCCESS
    }
    }

    /**
    * @this {Runno.WASI}
    * @param {Runno.WASI['fd_write']} origFdWrite
    * @returns {Runno.WASI['fd_write']}
    */
    function wrapFdWrite(origFdWrite) {
    return (...args) => {
    const [fd, ciovs_ptr, ciovs_len, retptr0] = args
    if (fd !== 1 && fd !== 2) return origFdWrite(...args)

    const view = new DataView(this.memory.buffer)
    const iovDescs = collectIOVectors(view, ciovs_ptr, ciovs_len)
    const iov = readIOVectorsMerged(view, iovDescs)

    if (iov.byteLength === 0) {
    return Result.SUCCESS
    }

    const stdfn = fd === 1 ? this.context.stdout : this.context.stderr
    stdfn(/** @type {any} */(iov))

    // FIXME: missing pushDebugData
    view.setUint32(retptr0, iov.byteLength, true)
    return Result.SUCCESS
    }
    }

    /** @param {Runno.WASI} wasi
    * @param {(timeout: number) => boolean} maybeYieldFunc */
    export function patchImportObject(wasi, maybeYieldFunc) {
    const { wasi_snapshot_preview1, ...impObjRest } = wasi.getImportObject()

    const origFdRead = wasi_snapshot_preview1.fd_read
    const origFdWrite = wasi_snapshot_preview1.fd_write
    const origPollOneoff = wasi_snapshot_preview1.poll_oneoff
    return {
    ...impObjRest,
    wasi_snapshot_preview1: {
    ...wasi_snapshot_preview1,
    fd_read: wrapFdRead.bind(wasi)(origFdRead),
    fd_write: wrapFdWrite.bind(wasi)(origFdWrite),
    poll_oneoff: wrapPollOneoff.bind(wasi)(origPollOneoff, maybeYieldFunc)
    }
    }
    }
    34 changes: 34 additions & 0 deletions sample.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,34 @@
    import * as Runno from '@runno/wasi'
    import { patchImportObject } from './patch-wasi'

    /** @typedef {{
    * stdin: (len: number) => Uint8Array | null,
    * stdout: (out: Uint8Array) => void,
    * stderr: (err: Uint8Array) => void,
    * }} MyStdioDef */

    /** @typedef {Partial<
    * Omit<Runno.WASIContextOptions, 'stdin' | 'stdout' | 'stderr'> & MyStdioDef
    * >} PatchedRunnoWASIContextOptions */

    /** @param {PatchedRunnoWASIContextOptions} opt
    * @returns {Runno.WASIContextOptions} */
    function definePatchedRunnoWASIContextOptions(opt) {
    return /** @type {Runno.WASIContextOptions} */(
    /** @type {unknown} */ (opt))
    }


    const wasi = new WASI(definePatchedRunnoWASIContextOptions({
    stdout(buf) {
    console.log(buf.byteLength)
    }
    /* ... */
    })

    const importObject = patchImportObject(wasi, timeout => {
    // let waitDuration = timeout < 0 ? Infinity : Math.max(timeout - Date.now(), 0)
    // return stdinReader.pollRead(waitDuration)
    })

    const wasm = await WebAssembly.instantiateStreaming(fetch('...'), importObject)
    145 changes: 145 additions & 0 deletions wrap-poll-oneoff.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,145 @@
    import * as Runno from '@runno/wasi'
    const { Result } = Runno.WASISnapshotPreview1

    const EventType = /** @type {const} */({
    CLOCK: 0,
    FD_READ: 1,
    FD_WRITE: 2,
    })

    const SubscriptionClockFlags = {
    SUBSCRIPTION_CLOCK_ABSTIME: 1,
    }

    const SUBSCRIPTION_SIZE = 48
    const EVENT_SIZE = 32

    /** @typedef {{
    * type: typeof EventType.CLOCK,
    * id: number, timeout: number, userdata: Uint8Array, precision: number,
    * }} ClockSubscription */
    /** @typedef {{
    * type: typeof EventType.FD_READ | typeof EventType.FD_WRITE,
    * fd: number, userdata: Uint8Array,
    * }} ReadWriteSubscription */

    /** @param {Uint8Array} userdata
    * @param {number} error
    * @returns {Uint8Array} */
    function createClockEvent(userdata, error) {
    const eventBuffer = new Uint8Array(EVENT_SIZE);
    eventBuffer.set(userdata, 0);

    const view = new DataView(eventBuffer.buffer);
    view.setUint16(8, error, true);
    view.setUint16(10, EventType.CLOCK, true);

    return eventBuffer;
    }

    /**
    * @this {Runno.WASI}
    * @param {Runno.WASI['poll_oneoff']} origPollOneoff
    * @param {(timeout: number) => boolean} pollStdin
    * called with -1 or a timeout, should (-1) block or (timeout) return whether stdin is ready
    * @returns {Runno.WASI['poll_oneoff']}
    */
    export default function wrapPollOneoff(origPollOneoff, pollStdin) {
    return (...args) => {
    const [in_ptr, out_ptr, nsubscriptions, retptr0] = args

    const subs = []
    for (let i = 0; i < nsubscriptions; i++) {
    const subscriptionBuffer = new Uint8Array(
    this.memory.buffer,
    in_ptr + i * SUBSCRIPTION_SIZE,
    SUBSCRIPTION_SIZE
    );
    subs.push(readSubscription(subscriptionBuffer));
    }

    let stdinIsReady = true

    const readStdinSub = /** @type {ReadWriteSubscription | undefined} */(
    subs.find(s => s.type === EventType.FD_READ && s.fd === 0))
    /** @type {ClockSubscription | undefined} */
    const clockSub = subs.find(s => s.type === EventType.CLOCK)

    // XXX: only handles the two cases that occurs from GHC RTS
    if (readStdinSub) {
    if (subs.length === 1 && clockSub === undefined) {
    // pure (blocking) fd_read
    pollStdin(-1)
    } else if (subs.length === 2 && clockSub !== undefined) {
    // fd_read + clock
    stdinIsReady = pollStdin(clockSub.timeout)
    }
    } // TODO: handle the case that other fds are queried

    if (!stdinIsReady) {
    // only reports the clock

    const eventBuffer = new Uint8Array(
    this.memory.buffer,
    out_ptr,
    EVENT_SIZE
    );

    eventBuffer.set(
    createClockEvent(/** @type {ClockSubscription} */(clockSub).userdata, Result.SUCCESS)
    )

    const returnView = new DataView(this.memory.buffer, retptr0, 4);
    returnView.setUint32(0, 1, true);
    return Result.SUCCESS
    }

    return origPollOneoff(...args)
    }
    }

    /** @param {Date} date */
    function dateToNanoseconds(date) {
    return BigInt(date.getTime()) * BigInt(1e6);
    }

    /**
    * @param {Uint8Array} buffer
    * @returns { ReadWriteSubscription | ClockSubscription } */
    function readSubscription(buffer) {
    const userdata = new Uint8Array(8);
    userdata.set(buffer.subarray(0, 8));

    const type = buffer[8];

    // View at SubscriptionU offset
    const view = new DataView(buffer.buffer, buffer.byteOffset + 9);
    switch (type) {
    case EventType.FD_READ:
    case EventType.FD_WRITE:
    return {
    userdata,
    type,
    fd: view.getUint32(0, true),
    };
    case EventType.CLOCK:
    const flags = view.getUint16(24, true);
    const currentTimeNanos = dateToNanoseconds(new Date());
    const timeoutRawNanos = view.getBigUint64(8, true);
    const precisionNanos = view.getBigUint64(16, true);

    const timeoutNanos =
    flags & SubscriptionClockFlags.SUBSCRIPTION_CLOCK_ABSTIME
    ? timeoutRawNanos
    : currentTimeNanos + timeoutRawNanos;

    return {
    userdata,
    type,
    id: view.getUint32(0, true),
    timeout: Number(timeoutRawNanos) / 1e6,
    precision: Number(timeoutNanos + precisionNanos) / 1e6,
    };
    default: throw new Error('invalid event type' + type)
    }
    }