C:\2010S\WEBASSEM.TXT

2017

WebAssembly: A Small Machine Inside the Browser

WebAssembly's 2017 deployment milestone gave browsers a shared binary execution format whose validation guarantees were useful and deliberately limited.

7 min readCovers February 2017 end of Browser Preview; 2019 standard distinguishedSoftware That Shaped the 2010s

A browser already knows how to display a page. Give it an image editor, a game or a substantial numerical calculation, and displaying the page becomes a fairly small part of the work.

Such applications need an execution environment. They also benefit when existing code can be compiled to a target shared by different browsers, rather than rewritten around one particular browser’s implementation.

WebAssembly supplied a compact, low-level format for that job. Its name invites a misleading picture of downloaded native machine code having free access to the computer. The useful engineering story is more constrained: a defined abstract machine, a validated module, and an explicit boundary to the host that runs it.

The browsers agreed to begin shipping

On February 28, 2017, Luke Wagner posted an announcement signed with Ben Titzer, Filip Pizlo and Abhijith Chatra. Representatives of Chrome, Edge, Firefox and WebKit had reached consensus on the initial WebAssembly API and binary design. The Browser Preview was ending; implementations could begin shipping it enabled by default.1

That is a deployment and design-consensus milestone. It does not establish that four browser releases shipped simultaneously on that day. The message also placed formation of a W3C Working Group and specification work among the next steps.

Formal standardization followed. On December 5, 2019, W3C announced three WebAssembly specifications as Recommendations.2 A browser capability can be deployed before its specification reaches that formal status. Keeping the two events separate explains why 2017 belongs in this series without changing the standard’s date.

The initial release was deliberately an MVP, a minimum viable product. Later features should not enlarge its historical inventory. Threads, SIMD, newer reference capabilities and later host interfaces are outside the 2017 scope discussed here.

A module crosses validation before execution and host interaction

Original explanatory diagram, 2026. A conceptual browser-hosted module pipeline; not a trace from a particular 2017 browser.

A calculation small enough to inspect

An original example will make the machine visible. Suppose an exported function computes:

f(x)=5x+2.f(x)=5x+2.

For an input of 3, the intended answer is 17. This is not a performance showcase. The calculation is small enough that we can examine every instruction and distinguish a valid program from a correct one.

We use a function that accepts one 32-bit integer and returns one 32-bit integer. Its parameter is local number zero. The function places values on an operand stack, performs arithmetic, then returns the remaining result.

The following readable instruction sequence uses the spelling in the later core 1.0 specification. Earlier text syntax used get_local where this later notation uses local.get; the binary instruction is the point of the example.3

local.get 0
i32.const 5
i32.mul
i32.const 2
i32.add

For x = 3, start with an empty stack. Retrieve the parameter and the stack holds 3. Push the constant 5 and it holds 3, 5. Multiplication consumes both and produces 15. Push 2, then add, leaving 17.

After instructionOperand stack, bottom to top
local.get 03
i32.const 53, 5
i32.mul15
i32.const 215, 2
i32.add17

The table is a teaching view of the instruction semantics, not a claim that a browser interpreter literally maintains this visible list. An engine can compile the instructions while preserving their meaning.

A stack representation is useful here because the instructions needn’t name temporary variables for the multiplication and addition. Their inputs and outputs connect through the stack. That connection is also something a validator can check.

The module says how to call the function

A body of arithmetic instructions is not yet the whole module. The accompanying original checker constructs a small binary module with a function type, a function entry, an export named affine, and its code body.

The export gives the host a name through which to call the function. The type describes what it accepts and returns. Those are distinct from the calculation itself: a host needs a callable boundary as well as valid instructions inside it.

Our example has no imports, memory, table or start function. It neither reads a document nor writes a pixel. The absence makes the boundary unusually easy to inspect. A host supplies an integer, calls affine, and receives an integer.

In the checker, the module is validated, compiled and instantiated using an installed modern Node.js runtime. The host calls the export with 3 and receives 17. Inputs zero and negative three produce 2 and negative thirteen. A large integer case is also checked against the corresponding 32-bit arithmetic.

This actual execution confirms the supplied bytes and outputs in Node.js 26.8.1. It does not prove that we ran a 2017 browser, a historical compiler or a period benchmark. The example uses a small core operation set and documents the later specification notation precisely so those boundaries remain visible.

Validity has a narrower job than correctness

Now change one instruction. Replace the 32-bit addition with 64-bit addition while leaving the rest of the function alone.

At that point the stack contains two 32-bit values. The replacement instruction requires two 64-bit values. The module fails validation in our check. It does not reach execution and then hope the incompatible operands happen to make sense.

The specification’s validation rules describe the type requirements of instructions and their effect on the stack. The arithmetic operation consumes operands of the required type and produces the declared result type.3 A valid sequence must fit together.

Now make a different change: replace the constant 5 with 6. The function still accepts a 32-bit integer, multiplies it by a 32-bit constant, adds another compatible constant and returns the expected type.

This altered module validates successfully. For input 3, it returns 20.

The answer is wrong for our intended formula and entirely consistent with the program we supplied. A validator can reject incompatible instruction types. It cannot infer that the person writing a constant really meant a different number.

VariantValidates?Result for input 3
Intended 5x + 2Yes17
Incompatible 64-bit addNoNo execution
Mistaken 6x + 2Yes20

This distinction is worth keeping in view whenever a platform describes its code as safe. A meaningful safety guarantee has a defined scope. It can prevent certain classes of malformed execution while leaving ordinary application mistakes possible.

Three modules separate validation failure from a valid incorrect calculation

Original explanatory diagram, 2026. These outcomes were checked with the accompanying hand-built modules in modern Node.js. No historical benchmark is implied.

Thirty-two bits are part of the contract

The formula also gives us a useful way to discuss arithmetic limits. Ordinary mathematical integers have no fixed upper boundary. A 32-bit integer representation has a fixed number of bit patterns.

For the small inputs above, the distinction doesn’t affect the answer. At larger values, it does. Our checker deliberately compares the WebAssembly result with JavaScript’s 32-bit multiplication operation and a final signed 32-bit coercion, rather than assuming all arithmetic behaves like unbounded algebra.

That choice makes the check about the actual represented calculation. If an application needs exact arbitrarily large integers, simply choosing this function signature would be insufficient. The programmer must choose an appropriate representation and implementation.

The lesson is smaller than a warning against low-level code. A portable execution format makes these semantics part of a shared contract. Application authors still have to decide whether that contract expresses their intended problem.

The three variants together separate several questions that are often bundled into one claim: can the module be decoded, is its instruction sequence well typed, can it be instantiated, and does its output satisfy the application’s requirements? Passing one stage does not answer every subsequent question.

The host remains a participant

A calculation that returns 17 doesn’t update a page by itself. Browser interaction needs a connection to the hosting environment. WebAssembly modules can use imports and exports to communicate; direct DOM manipulation is not an instruction in the core arithmetic machine.3

For an imagined page, JavaScript might obtain a user’s input, call the calculation and put its returned value into a visible field. That is a division of responsibility: the module performs a computation and the host handles the page interface.

Larger applications make more complicated exchanges. They must agree on how to represent data, when calls happen and who owns the relevant state. Crossing an interface has work associated with it, so a tiny calculation like ours would be a poor basis for declaring that every program becomes faster when moved to WebAssembly.

No timing result appears here. A five-instruction arithmetic example explains execution and validation far better than it explains application performance. Performance depends on the whole job, including data movement, startup and the host boundary.

A format that several implementations could share

The 2017 consensus mattered because an application target became a shared implementation commitment across major browser teams. It gave compiler and application work a clearer destination while allowing more experience to shape subsequent development.

The abstraction was deliberately modest. A module brought code in a defined format; validation checked structural and type constraints; an engine supplied execution; a host supplied the environment around it.

Our small function shows why that modesty is useful. We can explain the intended calculation, inspect the binary’s behavior, reject a type mismatch and still demonstrate a perfectly valid mistake.

WebAssembly made a carefully specified machine available inside a much larger piece of software. The machine could give firm answers about certain properties of a program. Choosing the right program remained somebody else’s job.

Footnotes

  1. Luke Wagner and co-signatories, WebAssembly consensus and end of Browser Preview, February 28, 2017. Full announcement read; permission to ship distinguished from browser release dates. ↩

  2. W3C, WebAssembly specifications become Recommendations, December 5, 2019. ↩

  3. Andreas Rossberg, editor, Core WebAssembly specification, version 1.0, 2019 version, later formatted publication. Sections on local instructions, validation, numeric encodings and the text-notation rename inspected. Original checker and examples are modern; notation is not presented as an exact 2017 listing. ↩ ↩2 ↩3

software-historywebassemblybrowserscompilersstandards