ReviewOS

zig-utils/zig-js

Private ABI: URL native record and component getters (fromString/deinit/href/protocol/username/password/search/host/hostname/port/pathname/hash/fragmentIdentifier)

#308
Closed chris opened this 9 days ago ·
9 days ago

Parent: #140 Tracked by: #134, #163, #164

Symbols (sub-slice 1 of the URL cluster)

SymbolHome (packages/runtime/src/jsc/URL.zig @ 7ed99c02)Bun C++ (upstream/src/jsc/bindings/BunString.cpp)
URL__fromString:33 — (*String) ?*URL:677 — parse, null when invalid, new WTF::URL
URL__deinit:43 — (*URL) void:692 — delete url
URL__href:35 — (*URL) String:697 — url->string() (with fragment)
URL__protocol:34:687 — scheme, no colon
URL__username / URL__password:36 / :37:702 / :707
URL__search:38:712 — query WITH leading ?
URL__host:39:724 — host WITHOUT port (not JS host)
URL__hostname:40:736 — host WITH port (not JS hostname)
URL__port:41 — (*URL) u32:741 — maxInt(u32) when unset, else ≤ u16
URL__pathname:45:752
URL__hash:30:661 — fragment WITH leading #
URL__fragmentIdentifier:31:669 — fragment WITHOUT #

The zig-js engine ships a real WHATWG-style URL implementation (urlParse with base/relative resolution, path/query/fragment state machine, UrlParts, live URLSearchParams linkage), so this slice is thin ABI plumbing over existing engine semantics — the same pattern as #307. Remaining URL-cluster symbols (URL__fromJS, URL__getHref*, URL__getFileURLString, URL__pathFromFileURL, BunString__toURL/toJSDOMURL, DOMURL__*, URL__originLength) are follow-up sub-slices.

Acceptance criteria

  • URL__fromString parses the BunString through the engine's urlParse (no base) and returns a native parsed-URL record, null for invalid input; the record owns its component bytes independently of any realm/arena.
  • URL__deinit frees the record; null tolerated; component getters tolerate null by returning Dead (Bun UB on null deref → zig-js safe sentinel); port → maxInt(u32).
  • Component getters match Bun/WTF semantics exactly: protocol without colon, search with leading ? (present-but-empty query keeps ? — deliberately NOT the JS search getter, which the WHATWG DOM spec maps to ""), host without port, hostname with port, port as u32 or maxInt(u32), hash with # only for non-empty fragments, fragmentIdentifier without.
  • Returned Strings are owned BunStrings (wtf_string_impl tag) with lifetimes independent of the record, matching WTF refcount semantics.
  • Engine parse layer (urlParse/urlSerialize/urlNormalizePort/urlParsePathQueryFrag/urlEncodeNormalizePath/urlParseRelative/urlPercentEncode/wtf8ToUtf8Bytes/urlOrigin) refactored from *Interpreter to allocator-first with zero behavior change — the functions used the interpreter only for self.arena; all existing call sites (URL builtin, fetch Request/Response/redirect) pass self.arena.
  • Compiled runtime fixture covers: parse→getters roundtrip on a full special URL (userinfo, port, path, query, fragment) with JS new URL parity where semantics overlap, default-port elision, non-special opaque-path URL, present-but-empty query/fragment, invalid input → null, null tolerance, record-lifetime independence after a second parse, UTF-16 BunString input. Fixture symbol count 294 → 307.
  • Home inventory 304/431, Bun inventory 297/421; bun-private-abi-audit + c-api-audit green; full zig build test zero new failures (worktree baseline-vs-candidate against the same pinned base).

Design constraints

  • *URL is a NATIVE heap record (c_allocator, magic-validated like PrivateStringBuilder), NOT a JS object — Bun's WTF::URL* has no realm, and URL__fromString carries no global. The JS-facing casts (URL__fromJS, DOMURL) are the follow-up sub-slice.
  • No punycode/IDNA work is pulled in: whatever the engine's parser does with non-ASCII hosts today is the pinned behavior (documented in the fixture).
  • URL__originLength stays deferred: Home itself declares it un-wrapped (needs bun.strings.firstNonASCII); assess separately.
9 days ago

Evidence — closed as completed

Commits (on main):

  • e56f5843 feat(abi): expose the URL native record and component getters (#308)
  • 062ef265 docs(abi): publish URL native-record boundary evidence (#308)

Implementation

  • src/interpreter.zig: zero-behavior-change refactor of the WHATWG parse layer — urlParse, urlSerialize, urlNormalizePort, urlParsePathQueryFrag, urlEncodeNormalizePath, urlParseRelative, urlPercentEncode, urlOrigin, wtf8ToUtf8Bytes now take std.mem.Allocator instead of *Interpreter (they only ever used self.arena); all 22 existing call sites (JS URL builtin, fetch Request/Response/redirect) pass self.arena. urlParse/urlSerialize/UrlParts are now pub.
  • src/c_api.zig: PrivateUrl — a magic-validated native heap record (c_allocator, one backing allocation holding href + every component), the exact WTF::URL* mapping; plus privateBunStringWTF8Alloc (context-free BunString→WTF-8 decode) and the 13 exports. Getters follow Bun/WTF semantics where they diverge from the JS getters: protocol without colon, search keeps ? for present-but-empty query, host without port / hostname with port (Home's documented inversion), port maxInt(u32) when unset, hash only for non-empty fragments. Null/stale handles return Dead/zero sentinels instead of Bun's UB; deinit clears the magic before freeing.

Validation

  • zig build test-home-private-abi → Home private value shims: 307/307 symbols linked; runtime matrix passed
  • Fixture coverage: full special URL (userinfo/port/dot-segment/path-encoding/query/fragment) with JS new URL parity on href/username/password/pathname/search/hash, WTF-semantics hand asserts on protocol/host/hostname/port/fragmentIdentifier, default-port elision, opaque-path mailto:, present-but-empty query ("?" native vs "" JS — deliberate divergence, both asserted), absent vs empty fragment, UTF-16 BunString input, invalid inputs (not a url, http://[::1, empty) → null, record independence across parses, null tolerance, deinit of all records + null.
  • bun-private-abi-audit → implemented-private=297, pending-private=124; Home audit implemented-private=304, pending-private=127; c-api audit 117/117 + 313 private exports; test-home-public-abi 50/50; zig build test -Dtest-filter=c_api 148/148; focused URL/fetch/TextEncoder engine tests green.
  • Full zig build test worktree A/B vs pinned base 939dae44: both sides 1148 passed / 3 failed with the byte-identical failure set (the three known pre-existing gc/wasm.decode/wasm.validate failures) — zero new failures; the 8 URL/fetch/encoding tests OK on both sides.

Sign in to comment on this issue.