Skip to content

Load a WebAssembly module a page declares - #17

Closed
pathscale wants to merge 2 commits into
masterfrom
feat/wasm-script-tag
Closed

pathscale wants to merge 2 commits into
masterfrom
feat/wasm-script-tag

Conversation

@pathscale

Copy link
Copy Markdown
Owner

A page can now hand its interface to a WebAssembly guest, with no JavaScript
anywhere in the path:

<script type="application/wasm" src="/demo.wasm" mount="#root"></script>

Until this, the only way to run a guest was chuzz --wasm /abs/path.wasm. That
tag described a loading mechanism no browser implemented, including this one.

Confirmed against the live site

$ chuzz-gui https://vliw-12345.xyz/
chuzz: wasm: mounted a guest on "#root", replacing 17 fallback node(s)

Real page, real module, end to end: fetched and parsed, tag found, src
resolved and confirmed same-origin, module fetched and vetted, #root located,
guest instantiated under wasmi, entry export returned OK, and only then the 17
fallback nodes removed. The module checks out independently too —
content-type: application/wasm, 708 bytes, magic 0061736d.

The same URL in Chrome shows the fallback unchanged: curl returns an <h1>
and explanatory prose inside #root, which browsers render because the tag is
inert to them. Not a blank page.

The fallback is the reason for most of the care

It is what a browser without wasm shows, so every way of failing has to leave it
standing. Verified across all four, with the success case in the same test so
the three "intact" assertions cannot pass merely because nothing ever mounts:

Case Fallback
selector matching nothing intact
bytes that are HTML, not a module intact
valid wasm, entry export missing intact
valid module, #root replaced, .panel present

Failure re-parses rather than repairs. A guest appends into the mount as it
builds, so a run that fails part way leaves its own half-built tree beside the
fallback with no way to tell them apart afterwards. Parsing the same bytes again
is the only way to be certain the document is what the page said. The mount is
emptied only after the entry export returns OK.

Bytes are vetted before wasmi sees them

The site this exists for sits behind a CDN that answers 200 with its index
page
for every unknown path, so a missing module arrives as <!doctype html>
and wasmi reports byte offsets in a file that is not a module. That has to say
the server returned HTML: the fix is a deploy, not a rebuild. A genuinely
corrupt module says something else, and the test pins the two apart.

Same origin only

The guest runs against the host ABI with the whole document reachable — a
different trust decision from a local file named on the command line.
Cross-origin needs a policy, not a default.

runtime="..." is parsed and ignored. Nothing implements preloaded runtimes
yet and the live page carries runtime="solid@1", so failing on it would reject
the page this exists to serve.

Structure

run_guest was split: run_guest_bytes holds the whole instantiate sequence
and run_guest reads a file and calls it. One copy rather than two, so the note
on why it is instantiate_and_start and not instantiate keeps applying to the
only copy. Both existing callers keep their signatures.

Verified

--wasm, --capture-wasm and --capture all demonstrated still working, not
assumed. 24 tests, clippy clean, fmt clean, and the --no-default-features
build without wasm compiles (it keeps the fallback and says why).

Known gaps

  • --capture does not take this path. It loads through document_loader,
    not browser.rs, so capturing that URL renders the fallback. That matters for
    producing a chrome-free still of the page.
  • Content-Type is not checked. blitz_net::fetch_async returns
    (String, Bytes) with no headers. Status is covered (non-2xx becomes
    ProviderError::HttpStatus) and magic bytes are covered; the distinct "server
    returned HTML" message comes from sniffing instead. A real MIME check needs a
    header-returning fetch in blitz-net.
  • A guest returning OK having built nothing empties the fallback and
    leaves a blank mount. run_guest_bytes warns but succeeds.
  • No events. The page renders and does not respond; that needs the guest's
    Store kept alive past the mount.

meh added 2 commits August 15, 2026 19:37
`run_guest` read the file and instantiated in one body, so a caller with
bytes already in hand had nowhere to enter. A module fetched from a page
never touches the disk.

`run_guest_bytes` is that entry point and now holds the whole sequence:
validate, link, `instantiate_and_start`, call the entry export, check the
status, warn when a guest reports OK without mutating anything.
`run_guest` reads the file and calls it.

One copy rather than two, deliberately. Everything that decides whether a
guest ran correctly is subtle enough to be worth having in a single place
-- the note on why this is `instantiate_and_start` and not `instantiate`
has to keep applying to the only copy of that call.

No behaviour change: both existing callers keep the signature they had.
A page can now hand its interface to a WebAssembly guest:

  <script type="application/wasm" src="/demo.wasm" mount="#root"></script>

The module is fetched with the page, vetted, and mounted against the
parsed document. No JavaScript anywhere in the path: a page that declares
a module is parsed without a script runtime, because attaching an engine
to run none is dead weight.

This is the other shape from `--wasm`. That replaces the document
entirely -- an empty `<html><body>` the guest fills, no HTML at all. Here
the document exists first, including whatever sits inside the mount, and
the guest takes that element over.

The fallback is the reason for most of the care here. It is what a
browser without wasm shows, so every way of failing has to leave it
standing: a bad selector, a 404, a server answering 200 with its index
page, a module that instantiates and then reports a non-OK status. The
mount is emptied only after the entry export has returned OK.

Failure re-parses rather than repairs. A guest appends into the mount as
it builds, so a run that fails part way leaves its own half-built tree
beside the fallback with no way to tell them apart afterwards. Parsing
the same bytes again is the only way to be certain the document is what
the page said.

Bytes are vetted before wasmi sees them, because the failure that catches
is not a corrupt module. The site this exists for is behind a CDN that
answers 200 with its index page for every unknown path, so a missing
module arrives as `<!doctype html>` and wasmi reports byte offsets in a
file that is not a module. That has to say the server returned HTML: the
fix is a deploy, not a rebuild.

Same origin only. The guest runs against the host ABI with the whole
document reachable, which is a different trust decision from a local file
named on the command line. Cross-origin needs a policy, not a default.

`runtime="..."` is parsed and ignored. Nothing implements preloaded
runtimes yet and the live page carries the attribute, so failing on it
would reject the page it is meant to serve.
@pathscale

Copy link
Copy Markdown
Owner Author

Folded into #19, which now carries this whole stack against master.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant