holger-plugin-abi 0.1.0

holger package-handler plugin ABI: the wire contract a handler speaks whether it is linked in or loaded from a .wasm
Documentation
  • Coverage
  • 42.96%
    61 out of 142 items documented0 out of 84 items with examples
  • Size
  • Source code size: 52.75 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.11 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 3s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • Ignalina

holger's package-handler plugin contract — the one wire format a handler speaks whether it is linked into the server or loaded from a .wasm at startup.

Rickard's ask was that every package handler should be writable as native or WASM. Before this crate holger had no wasm path at all: the sixteen repository backends are unconditional path dependencies of server/lib, so adding a package handler meant editing server/lib/Cargo.toml and rebuilding the server.

The two seams

A RepositoryBackendTrait is a Rust trait object and cannot cross a wasm boundary, so this crate splits a handler in two along the line that actually matters:

  • [PackageHandler] — the format logic: coordinate parsing, the HTTP path scheme, listing, content types. This is what varies per ecosystem and what a plugin author writes. It is pure and target-independent, so the one implementation compiles for the host and for wasm32-unknown-unknown.
  • [BlobStore] — the bytes. A wasm module has no filesystem and no socket. The host owns storage and hands it in.

That split is what makes "the wasm crate is ABI glue only" true rather than aspirational: the native backend implements BlobStore over the filesystem, the wasm shim implements it over four host imports, and both call the same PackageHandler. It is the same shape as znippy's maven pair, where host-decompressors compiles the threaded ljar fan-out out of the wasm build and leaves the single-threaded linflate both sides share — same logic, different plumbing underneath.

Which verbs cross, and why

RepositoryBackendTrait (traits/src/lib.rs:404) has twelve methods. Six cross:

verb why it is in
plugin_manifest the module must name itself, or registration needs a config file and the whole point of scanning a directory is lost
fetch the read. Non-defaulted on the trait.
put the write. Non-defaulted on the trait.
list enumeration; the UI and retention planner both read it
coordinate_for_path the serve-time quarantine gate calls it before bytes go out, and it is pure path logic — the cheapest and most format-specific thing a handler owns
handle_http2_request non-defaulted, and it is the actual door a package client knocks on. A handler that could not answer it would not be a package handler.

Six do not, and the host supplies the trait's own default for each:

verb why it is out
name / format / is_writable constants. Read once from the manifest at load, not per call.
archive_files / archive_info / has_archive these describe a znippy archive handle. A wasm module has none, and the trait already defaults them to "no archive" — which is the truth here, not a stub.
delete_artifact the trait default fails closed, and the contract requires the implementer to recompute the stored digest and refuse a mismatch. A sandboxed module cannot be trusted to have done that, and a wrong answer deletes bytes. Failing closed is the correct answer, not a missing feature.

ABI shape

Every exported verb takes a (ptr, len) pair into linear memory and returns a pointer; the byte length of that result is read back with result_len(), exactly as znippy's loader does. Payloads are length-prefixed ([codec]) — never JSON, whose separators and missing null representation are a corruption source rather than a parser bug.