Template Compiler (DRAFT)

Compiling HTML (or other) templates to Wasm Components

WebAssembly (Wasm) components are a compact, portable, and secure unit of code. They're a compile target with a binary format, not a source code language. So you won't write them by hand, you'll have tools to generate them for you.

In addition to languages like Rust, C++, JavaScript, and Python that are "General Purpose", certain Domain-Specific languages may be good candidates for "componentizing".

For example,

Template-to-Component Compiler

Let's focus on the second example and make a compiler that converts templates into components!

A Wasm component has a type that's defined by a "world" in the WIT interface-definition language which identifies all the things the component imports and exports. For simple templates, we don't need any imports and we have exactly one export which is our templating function.

world my-template {
record params {
title: string,
...
}

export apply: func(param: params) -> string
}

The exact fields and types that the parameters record has will depend on what our template uses and we can infer that directly from the template file.

Canonical ABI

To implement this high-level interface, we need to use the canonical ABI which defines the way that high-level component types can be passed into and returned from components using integer values and the Wasm linear memory.

The canonical ABI connects the component model and core wasm

Lifting and Lowering

In the Canonical ABI, the Component Model is higher (as in higher-level) than Core Wasm and Modules. So, when things need to be converted upwards from Core Wasm to the Component Model, it's called lifting. Conversely, when things need to be converted downards from the Component Model to Core Wasm, it's called lowering.

Lifting types from Core Wasm to the Component Model and lowering them from Component Model to Core Wasm

Imported and Exported Functions

When talking about functions, the direction of lifting and lowering corresponds to whether the function is an export or import.

Exported functions are defined in the inner module and lifted to the component which re-exports it. Imported functions are defined by a component import and lowered to the module import.

Diagram showing module exports being lifted to component exports, and component imports being lowed to module imports

For exports and imports, the arguments go in the direction from caller to callee and the returns go from callee to caller.

Diagram showing export arguments being lowered and return being lifted, with import arguments being raised and return being lowered

Values

Values in the canonical ABI are either passed directly in arguments/returns as a sequence of core Wasm values (e.g. i32, f32) or indirectly using memory. The default is for values to be passed directly and memory indirection is used when the value is too large to be passed directly or the value is part of a list.

The canonical ABI allows components to select which string encoding to lift/lower strings from/into and we will be choosing UTF-8 (the other options are UTF-16 and Latin 1 + UTF-16). Strings in the canonical ABI are represented as an offset and length which has the direct representation (i32, i32) and a memory representation of two 4-byte little endian integers.

Diagram showing string memory being layed out with pointer and length together pointing at the text data

Records are represented directly by concatenating the direct representation of all their fields in order and are represented in memory by aligning and concatenating the memory representation of each field in order.

Template ABI

The generated template function is exported, which means it will be defined in the module then lifted and rexported in the outer component.

It has a single record argument, which is lowered into the template, and it has a single string result, which is lifted back up to the caller. Depending on the number of string parameters the parameter record will either be passed directly or in memory.

Allocators

In order to use the canonical ABI with arguments spilled to memory (which can happen depending on the number of parameters), we have to provide an allocator for the host to use for allocating the spilled args.

There are many kinds of allocators but because we're only ever using it to allocate arguments which are then all freed together we can use one of the simplest allocators called a bump allocator.

Generating the Module

To return a string, we just need to return the integer index in memory of the (index, length) pair.