Skip to content

Generating WIT/WebAssembly Component for WebTransport server (WebAssembly Core Module) #1362

Closed

Description

@guest271314

I spent about a year trying to get a WebTransport server working in the browser using Direct Sockets UDPSocket. With the help of a Zig programmer I realized that. Naturall

Zig WebTransport (TLS+QUIC state machine) server compiled to WASM standalone. From that WASM I compiled to JavaScript with wasm2js.

Now, I don't get all the hubbub about WebAssembly Components and WIT. Nevertheless I and interested in taking the WebAssembly Module and/or the asm.js output of wasm2js and creating a WebTransport "world" in WIT and thus port the WebTransport WebAssembly Core Module to WebAssembly Component Model, just for kicks, and because AFAICT there's no WebTransport WIT world already defined anywhere.

What do I need to do to take the existing WASM and/or JavaScript I have and create the WebAssembly Component thing?

Activity

  1. vados-cosmonic commented on Apr 6, 2026

    @vados-cosmonic
    Collaborator

    Glad you're making progress but this to be clear, this issue is actually outside the scope of Jco -- we deal with WebAssembly components (or taking JS to build them, but that is outsourced essentially to componentize-js) and generate code to enable running in node (and experimentally the browser, but that implementation is pretty incomplete as of now).

    That said, with regards to the work you're doing it seems like you may benefit from:

    Assuming you have added implementations to interfaces generated by wit-bindgen, hooked that to your Zig code, you should get a component that works and adheres to the WIT world that you create.

    I do not think you need to use wasm2js here, but if you insist then you'd have to convert your JS code to a component with a tool like componentize-js (or jco componentize).

    Zulip is the right place to discuss/bat around ideas on this, rather than an issue on the Jco repo -- maybe if we had discussions enabled or something that might also work but for now we do not.

    I'm going to close this out for now since it's not a Jco issue per-say, but we can keep discussing here or on Zulip

  2. guest271314 commented on Apr 6, 2026

    @guest271314
    Author

    @vados-cosmonic Well, I don't need WASI for this. To me looks like there's a bug in wasm-tools. Unles you can explain what's wrong with this WIT. jco throws at the same place, because jco depends on wasm-tools

    ../wasm-tools/wasm-tools component embed ./quic.wit ./quic.wasm -o quic.embedded.wasm
    

    That works.

    This throws

     ../wasm-tools/wasm-tools component new quic.embedded.wasm -o quic.component.wasm
    error: failed to encode a component from module
    
    package local:quic-wasm;
    
    world quic-wasm-world {
      import env: interface {
        console-log: func(argv: string);
        get-time-ns: func() -> u64;
        random-fill: func(argv: list<u8>);
      }
      export qz-set-cert: func(argv: list<u8>) -> u32;
      export qz-set-key: func(argv: list<u8>) -> u32;
      export qz-init-server: func() -> u32;
      export qz-init-client: func() -> u32;
      export qz-deinit: func();
      export qz-recv-packet: func(argv: list<u8>) -> u32;
      export qz-send-packets: func(argv: list<u8>) -> u32;
      export qz-on-timeout: func();
      export qz-next-timeout-ns: func() -> u64;
      export qz-is-established: func() -> u32;
      export qz-is-closed: func() -> u32;
      export qz-poll-event: func(argv: list<u8>) -> u32;
      export qz-stream-send: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-stream-close: func(argv: u64);
      export qz-stream-read: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-datagram-send: func(argv: list<u8>) -> u32;
      export qz-datagram-recv: func(argv: list<u8>) -> u32;
      export qz-wt-accept-session: func(argv: u64) -> u32;
      export qz-wt-read-stream: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-wt-send-stream: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-wt-close-stream: func(argv: u64);
      export qz-wt-read-datagram: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-wt-send-datagram: func(argv0: u64, argv1: list<u8>) -> u32;
      export qz-wt-close-session: func(argv0: u64, argv1: list<u8>, argv2: u32) -> u32;
      export qz-alloc: func(argv: u32) -> u32;
      export qz-free: func(argv: list<u8>);
    }
    
    
  3. guest271314 commented on Apr 6, 2026

    @guest271314
    Author

    Seems to me that you folks should have a very basic WIT generating script that accepts WAT file

    // WIT Generator Script
    // https://share.google/aimode/8MIUn7InsCSvw9Uxv
    const fs = require('fs');
    const path = require('path');
    
    const mapT = t => ({ i32: 'u32', i64: 'u64', f32: 'float32', f64: 'float64' }[t] || 'u32');
    
    function buildS(p, r, n) {
      let raw = [];
      for (let i = 0; i < p.length; i++) {
        if (i + 1 < p.length && p[i] === 'i32' && p[i+1] === 'i32') {
          raw.push(/log|name|path|uri/.test(n.toLowerCase()) ? 'string' : 'list<u8>');
          i++;
        } else raw.push(mapT(p[i]));
      }
      const ps = raw.length === 1 ? `argv: ${raw[0]}` : raw.map((t, i) => `argv${i}: ${t}`).join(', ');
      const res = r.map(mapT);
      const rs = res.length === 1 ? ` -> ${res[0]}` : res.length > 1 ? ` -> tuple<${res.join(', ')}>` : '';
      return `func(${ps})${rs}`;
    }
    
    function watToWit(p) {
      const c = fs.readFileSync(p, 'utf8');
      const base = path.basename(p, path.extname(p)).replace(/_/g, '-').toLowerCase();
      
      const types = {};
      [...c.matchAll(/\(type\s+\(;(\d+);\)\s+\(func\s+((?:\(param\s+[^)]+\)\s*)*)\s*((?:\(result\s+[^)]+\)\s*)*)\)\)/g)]
        .forEach(m => types[m[1]] = { p: m[2].match(/i32|i64|f32|f64/g) || [], r: m[3].match(/i32|i64|f32|f64/g) || [] });
    
      const f2t = {};
      [...c.matchAll(/\(func\s+(?:\$[^\s]+\s+)?\(;(\d+);\)\s+\(type\s+(\d+)\)/g)]
        .forEach(m => f2t[m[1]] = m[2]);
    
      let wit = [`package local:${base};`, '', `world ${base}-world {`];
      const ims = {};
      [...c.matchAll(/\(import\s+"([^"]+)"\s+"([^"]+)"\s+\(func\s+(?:\$[^\s]+\s+)?\(;(\d+);\)/g)]
        .forEach(m => {
          const [_, mod, field, fIdx] = m;
          if (!ims[mod]) ims[mod] = [];
          const t = types[f2t[fIdx]] || { p: [], r: [] };
          ims[mod].push(`    ${field.replace(/_/g, '-')}: ${buildS(t.p, t.r, field)};`);
        });
    
      Object.entries(ims).forEach(([m, fs]) => {
        wit.push(`  import ${m.replace(/_/g, '-')}: interface {`, ...fs, `  }`);
      });
    
      [...c.matchAll(/\(export\s+"([^"]+)"\s+\(func\s+(\d+)\)\)/g)]
        .forEach(m => {
          const [_, name, fIdx] = m;
          if (name === 'memory') return;
          const t = types[f2t[fIdx]] || { p: [], r: [] };
          wit.push(`  export ${name.replace(/_/g, '-')}: ${buildS(t.p, t.r, name)};`);
        });
    
      return wit.concat('}').join('\n');
    }
    
    const args = process.argv.slice(2);
    if (args.length > 0) console.log(watToWit(args[0]));
    
    
  4. guest271314 commented on Apr 6, 2026

    @guest271314
    Author

    Now, jco was able to generate the types. So I don't get why jco and wasm-tools broke on creating the component thingy

    // world local:quic-wasm/quic-wasm-world
    export type * as Env from './interfaces/env.js'; // import env
    export function qzSetCert(argv: Uint8Array): number;
    export function qzSetKey(argv: Uint8Array): number;
    export function qzInitServer(): number;
    export function qzInitClient(): number;
    export function qzDeinit(): void;
    export function qzRecvPacket(argv: Uint8Array): number;
    export function qzSendPackets(argv: Uint8Array): number;
    export function qzOnTimeout(): void;
    export function qzNextTimeoutNs(): bigint;
    export function qzIsEstablished(): number;
    export function qzIsClosed(): number;
    export function qzPollEvent(argv: Uint8Array): number;
    export function qzStreamSend(argv0: bigint, argv1: Uint8Array): number;
    export function qzStreamClose(argv: bigint): void;
    export function qzStreamRead(argv0: bigint, argv1: Uint8Array): number;
    export function qzDatagramSend(argv: Uint8Array): number;
    export function qzDatagramRecv(argv: Uint8Array): number;
    export function qzWtAcceptSession(argv: bigint): number;
    export function qzWtReadStream(argv0: bigint, argv1: Uint8Array): number;
    export function qzWtSendStream(argv0: bigint, argv1: Uint8Array): number;
    export function qzWtCloseStream(argv: bigint): void;
    export function qzWtReadDatagram(argv0: bigint, argv1: Uint8Array): number;
    export function qzWtSendDatagram(argv0: bigint, argv1: Uint8Array): number;
    export function qzWtCloseSession(argv0: bigint, argv1: Uint8Array, argv2: number): number;
    export function qzAlloc(argv: number): number;
    export function qzFree(argv: Uint8Array): void;
    
    
    /** @module Interface env **/
    export function consoleLog(argv: string): void;
    export function getTimeNs(): bigint;
    export function randomFill(argv: Uint8Array): void;
    
    
  5. vados-cosmonic commented on Apr 6, 2026

    @vados-cosmonic
    Collaborator

    Glad you were able to make progress -- not sure why wasm-tools component new didn't work for you (that seems like a bug that should be filed upstream if the failure was legitimate), but if you were able to get what you wanted working, great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions