MultiverseSocial · /dev/bml

BML v2.1 · Blitz v2

Byte-coded HTML5 and the wire that carries it. Big-endian everywhere, fixed-size packed records, no padding, no alignment. The reader looks up an op and branches to a handler. There is nothing to parse.

BML v2.1 Blitz v2 big-endian magic 42 4D 4C 02 · minor 00 01 Blitz magic "BLZ2" spec frozen · services staged

Stunt Grok Jockey Tony (@tswain555 on X)

What BML is

BML is HTML5 in bytecode form, built for headless Linux daemons and the SDL3 BlitzBrowse client. Each element and attribute has a one-byte id. Attribute values are typed. A stream is walked, not parsed.

Blitz is the transport. Its hot path:

read 4-byte header word -> handlers[op] -> read len bytes -> handler

The handler works on fixed-size big-endian records. blitz_hdr_unpack checks each op's minimum and maximum length before dispatch, so a handler never sees a bad length.

The library is plain C99 with no SDL or other GUI dependency.

Version, byte order & magic

ItemValue
BML stream'B' 'M' 'L' 0x02, then minor u16 BE 0x0001 = v2.1. Bytes: 42 4D 4C 02 00 01
Blitz v2HELLO / HELLO_ACK carry magic u32 "BLZ2" (42 4C 5A 32) plus caps u16
Byte orderBig-endian (network order) everywhere. No padding, no alignment.
RecordsFixed-size packed records. Fixed records must match their exact length.
Blitz TCP port23232 (also embeddable behind HTTP)

Byte order is defined in one place, blitz_wire.h. Read and write multi-byte values only with bw_get16/32/64/f64 and bw_put16/32/64/f64. Do not shift bytes by hand anywhere else.

Blitz v2 frame header

Each frame is a 4-byte header (one BE u32) followed by len bytes of payload.

bit 31..25  op    7 bits (1..127; 0 invalid)
bit 24      MORE  1 bit
bit 23..0   len   24 bits payload length

header word = op<<25 | MORE<<24 | len

The header has no reserved bits. The old v1 header was 8 bytes and had a 16-bit reserved field. Because the op sits in the top 7 bits, the first header byte is op<<1 | MORE. That is why HELLO (op 01) starts with 0x02 on the wire.

Length limits are enforced at unpack: BODY ≤ 16 KiB (16384), REQUEST ≤ 13 + 2048, and fixed records exact.

Ops and records

opnamepayload (all BE)
01HELLO6B: magic "BLZ2" u32, caps u16 (client → server)
02HELLO_ACK6B: same; caps = intersection (server → client)
03REQUEST13B + url: id u16, mth_acc u8, etag u32, body_len u32, url_len u16, url bytes. mth_acc = method(3 bits)<<5 | accept(5 bits). MORE=1: HEADER frames follow (the last has MORE=0). body_len>0: BODY frames + END follow the headers.
04HEADER1B + value: hid u8, value bytes (len−1)
05RESPONSE19B: id u16, status u16, ct_fl u8, max_age u16, etag u32, mtime u32, body_len u32. ct_fl = ctype(5 bits)<<3 | flags(3 bits). MORE=1: HEADER frames follow.
06BODYraw bytes, ≤ 16384
07ENDempty. Every response ends with exactly one END, including 304, HEAD and empty bodies.
08CLOSEempty
09 / 0APING / PONG4B: nonce u32
0BERROR2B: code u16. The sender closes after ERROR.

Field codes

FieldValues
method0 GET, 1 HEAD, 2 POST, 3 PUT
accept bits1 BML, 2 CHDD, 4 TEXT, 8 IMAGE, 0x10 ANY (wildcard). Mask 0 = all.
ctype0 bin, 1 bml, 2 chdd, 3 html, 4 text, 5 png, 6 jpeg, 7 gif, 8 json, 9 css, 10 js
response flags1 PUBLIC (shared cache ok), 2 NOBODY (HEAD/304), 4 LENUNK
caps1 BML, 2 CHDD, 4 KEEPALIVE, 8 CACHE, 0x10 PUT
header hid1 Host, 2 User-Agent, 3 Accept, 4 Client-IP, 5 Content-Type, 6 Auth, 7 CV-Pub, 8 CV-Sig, 9 Prefix, 10 Location
ERROR code1 magic, 2 frame, 3 too large, 4 unsupported, 5 timeout, 6 internal, 7 sequence
statusHTTP status numbers
etagu32 validator, blitz_etag(size, mtime), never 0. A request etag that matches the current one gets a 304.
max_ageseconds

Sequence

HELLO -> HELLO_ACK
  -> { REQUEST [HEADER*] [BODY* END]
       -> RESPONSE [HEADER*] BODY* END }*
  -> CLOSE

One connection carries many requests (keep-alive).

Golden vectors (excerpt)

These come from blitz_vectors.h, the shared golden test vectors. The Blitz vectors were derived by hand from the layout, not produced by the codec. All bytes are big-endian.

HELLO caps=0x001F
02 00 00 06  42 4C 5A 32  00 1F

HELLO_ACK caps=0x0007
04 00 00 06  42 4C 5A 32  00 07

REQUEST more=1 id=0x0102 GET accept=BML|TEXT etag=0xDEADBEEF body=0 url=/a.bml
07 00 00 13  01 02  05  DE AD BE EF  00 00 00 00  00 06  2F 61 2E 62 6D 6C

HEADER hid=1 (Host) value="h"
08 00 00 02  01 68

RESPONSE id=0x0102 200 BML|PUBLIC max_age=3600 etag=0xCAFEBABE mtime=0x65000000 body_len=0x123
0A 00 00 13  01 02  00 C8  09  0E 10  CA FE BA BE  65 00 00 00  00 00 01 23

END     0E 00 00 00
CLOSE   10 00 00 00
ERROR   16 00 00 02  00 03      (code 3, too large)

The test suite also includes vectors that must be rejected: op 0, unknown ops 12 and 127, a BODY of length 16385, a HELLO with length 5, an END with length 1, and a bad magic "BLZ1".

BML v2.1 stream

'B' 'M' 'L' 0x02 | minor u16 BE = 0x0001 | nodes
string = len u16 BE + UTF-8

Close events are always paired

bml_walk fires a close for every open. On the wire, void elements (br, img, input, …), DOCTYPE, comments and CDATA carry no END byte, so the walker synthesizes their close. A renderer that prints HTML should skip the closing tag when:

op == BML_OP_DOCTYPE || op == BML_OP_COMMENT || op == BML_OP_CDATA ||
bml_is_void(bml_element_info(op))

The full element and attribute opcode tables live in bml_tables.h / bml_tables.c and are not repeated here.

Over HTTP

Over HTTP/1.1, BML is sent as Content-Type: application/x-bml with ETag: "xxxxxxxx", using the same validator as the Blitz etag. It can be served by bmld --http or by Apache through mod_bml, a gateway that talks Blitz v2 upstream.

Using libbml

Headers

HeaderRole
bml_tables.hOpcodes, attr ids, value types, element/attr tables
bml.hRuntime: Cell/Table, handlers, bml_html_to_bml / bml_bml_to_html
bml_walk.hBounds-checked callback walker
bml_handlers.hPer-tag handler decls (optional if you only use the walker)
bml_min.hDrop-in BMLMinRunBuf / BMLMinRunFile
blitz_wire.hThe byte-order and wire constants header (big-endian), Blitz v2 layout
blitz_frame.hBlitz v2 frame/record codec (no I/O)

All includes are flat (#include "bml.h"). Compile with -I pointing at the BML folder.

Build

gcc -std=c99 -Wall -Wextra -Werror -pedantic -Ibml_port \
    -c bml_tables.c bml_walk.c bml.c bml_functions.c bmlo.c
ar rcs libbml.a bml_tables.o bml_walk.o bml.o bml_functions.o bmlo.o

You can also run ./build_portable.sh in bml_port/ (no make, no env vars). Add blitz_frame.c when you need the transport; it depends only on blitz_wire.h. After editing, run ./sync_bml.sh to refresh the bmld and bml_apache copies, and ./sync_bml.sh --check to verify them.

Walker

int bml_walk_check_header(const unsigned char *bw_buf, size_t bw_len);
int bml_walk(const unsigned char *bw_buf, size_t bw_len,
             const struct BmlWalkCbs *bw_cbs);

int bml_html_to_bml(const char *html, unsigned char **out, size_t *out_len);
int bml_bml_to_html(const unsigned char *bml, size_t bml_len,
                    char **out, size_t *out_len);   /* caller frees *out */

struct BmlWalkCbs has the callbacks bw_on_open, bw_on_attr, bw_on_text, bw_on_close and bw_on_error, plus bw_user.

ReturnMeaning
0 OKSuccess
-1 ARGNULL buffer/cbs or too short
-2 MAGICBad magic
-3 TRUNCTruncated stream
-4 DEPTHExceeded max depth
-5 OPCODEUnknown / invalid opcode
-6 ATTRBad attribute
-7 VALUEBad typed value
-8 ENDUnexpected OP_END

Constants are named BML_WALK_ERR_*. A wrong minor version is reported as BML_WALK_ERR_VERSION.

bmld and samples

bmld is a standalone BML server written as one C file plus the shared Blitz codec. It serves a docroot of pre-encoded .bml and other static files over the Blitz v2 TCP port and over plain HTTP/1.1 as application/x-bml. It has no TLS; terminate TLS in front of it. It does not convert HTML to BML on the fly, so it serves pre-encoded files only.

bmld --root DIR [--blitz PORT] [--http PORT] [--bind ADDR]
     [--no-blitz] [--no-http] [--max-age SECONDS] [--max-conns N]
     [--idle SECONDS] [-v]
bmld --root DIR --check

It takes options on the command line only and reads no environment variables. Tools in the source tree: html2bml, bml2html, bmlget, and the smoke client blitz_get.

Samples in bml_port/samples/ (mirrored in bmld/www/) are all v2.1 BE, so each one starts with 42 4D 4C 02 00 01:

From v1 to v2

BML v1.22 and little-endian Blitz v1 are superseded by BML v2.1 and Blitz v2, which are big-endian.

The formats are not wire-compatible. A Blitz v1 client or server cannot talk to a Blitz v2 one. bmld refuses legacy little-endian .bml files, and the v2.1 walker rejects v2.0 streams. Clients and servers need to move to v2 together.

Status

The spec is frozen. The decision (2026-10-03) is that all wire formats are big-endian, fixed-size packed records.

This page will be updated when these go live.

Hits--