Skip to content

feat(backend): use both backends in one build, chosen by type - #8

Merged
karstx merged 20 commits into
masterfrom
feat/runtime-backend-selection
Sep 3, 2026
Merged

karstx merged 20 commits into
masterfrom
feat/runtime-backend-selection

Conversation

@karstx

@karstx karstx commented Sep 2, 2026 •

Copy link
Copy Markdown
Owner

Lets a program use both backends, the OS HID stack and raw USB via nusb, in the same build, choosing per Hidra instance:

let native = Hidra::new()?;                    // the OS HID stack
let usb = Hidra::<Nusb>::builder().build()?;   // raw USB transfers

The backend used to be picked by Cargo feature, so enabling nusb anywhere in the graph changed which backend every crate got. It is a type parameter now: HidBackend and HidDeviceBackend are public, implemented by Native, NativeDevice, Nusb and NusbDevice. A backend this build does not have cannot be named, so choosing wrongly is a compile error that names the missing feature rather than a run-time Unsupported.

Holding either in one variable is the caller's job; examples/backends.rs is a complete one in about thirty lines, and sinowisp does the same in karstx/sinowisp#152.

Also in here

  • MSRV 1.85, nusb 0.2.7. nusb is in the default feature set, so its floor is the crate's; no 0.2.x line reaches 1.77.
  • The per-OS extensions are inherent to Native, so reaching for set_write_timeout through Nusb no longer compiles, and they lose the Result that only ever carried "unsupported under the other backend".
  • wasm docs were broken: the crate docs linked native-only items unconditionally, so every cargo doc --target wasm32-unknown-unknown failed. The docs job only builds native, so nothing caught it; the wasm job builds docs now too.
  • A control-only e2e target: a FunctionFS gadget declaring HID class with bNumEndpoints 0, the shape of the Sinowealth ISP bootloaders, which g_hid cannot produce. It pins down that write falls back to SET_REPORT(Output) and read refuses rather than blocking forever.
  • Packaging: cargo package includes untracked files, so the 419 MB VM image nix run .#test-vm leaves in the tree would have been published. An include whitelist fixes it (29 files, 97 KB). docs.rs was building Linux and wasm only, so the Windows and macOS APIs were missing from the rendered docs.

Testing

fmt, clippy, tests and both doc targets; cargo check for all four targets. The full Linux e2e matrix (uhid, nusb, control-only, WebHID) passes in the test VM.

`nusb::Device::control_in`/`control_out` are unavailable on Windows, so the
control-only arms failed to compile there once the nusb backend stopped being
feature-gated off. `open` already refuses a failed claim on Windows, leaving
the fallback unreachable; route it through cfg'd helpers mirroring
`read_report_descriptor_unclaimed`.
@karstx
karstx force-pushed the feat/runtime-backend-selection branch from 6e348cd to e56b493 Compare September 2, 2026 17:47
The hidraw backend was shared with Linux and nusb has no enumeration there,
so the target carried a cfg arm and a CI check without being exercised.
Both backends are additive and picked at run time, so building the USB
transport in by default makes `Backend::Nusb` selectable without a feature
flag. `--no-default-features` still drops it.
Enumerates through every available backend in one process and opens a
device by falling back from one to the next.
@karstx
karstx force-pushed the feat/runtime-backend-selection branch from 20b1560 to 7eb8aca Compare September 2, 2026 20:49
`HidraBuilder::enumerate(bool)` read as a sibling of `Hidra::enumerate(vid,
pid)` while doing something unrelated, and the two appeared within thirty
lines of each other in the conformance suite. Rename it to
`enumerate_on_build`, deprecate `new_without_enumerate` now that the builder
expresses it, and derive `Backend::default` from `available` so the
preference order lives in one place.
The rustup bootstrapper was pinned at the `dist/` URL, which is replaced in
place on each release, so its hash drifted and the toolchain layer stopped
building. Point it at the immutable `archive/1.29.1/` path, which serves
byte-identical content, matching how the VS bootstrappers are pinned.

Flakes read NIXPKGS_ALLOW_UNFREE only under `--impure`, so the documented
commands could not evaluate the ISO derivation.
nusb is in the default feature set, so its own floor is the crate's: no
0.2.x line reaches 1.77 (0.2.0-0.2.5 need 1.79, 0.2.6+ need 1.85), and
claiming 1.77 only bought a confusing resolver error on `cargo add`.

1.85's cargo reads the v4 lockfile, so the regenerate-lockfile step goes
too, and `is_none_or` is now available to the example.
The backend table and the paragraph about picking between backends link
Backend and Hidra::builder, neither of which exists on wasm, so every
cargo doc --target wasm32-unknown-unknown failed.

Nothing caught it because the docs job only builds the native target, so
the wasm job now builds docs too.
HID mandates an interrupt IN endpoint, but control-only devices that
declare bNumEndpoints 0 exist -- the Sinowealth ISP bootloaders are
exactly that, and their descriptor is the shape this reproduces. The
backend's fallback for them was untested, because g_hid always creates
an interrupt IN endpoint and so cannot produce the device; FunctionFS
can, since userspace supplies the descriptors verbatim.

The test pins down that enumeration still finds it, that reports go over
the control pipe both ways, that write falls back to SET_REPORT(Output),
and that read refuses rather than blocking forever on a report that can
never arrive.

That last error claimed the reader thread had terminated, when for this
shape there was never one to terminate; it now says what is actually
wrong.
cargo package sweeps in untracked files, so anything left in the tree
ships: the VM image nix run .#test-vm drops here is 419M, well past the
crates.io limit. An include whitelist is the only thing that reliably
keeps a stray artifact out; the image is gitignored as well, since it is
a build product.

docs.rs was also building Linux and wasm only, so the Windows and macOS
backends were absent from the rendered docs along with the API they carry
(container_id, set_write_timeout, set_open_exclusive).
The comments restated what the code already said, and the README had
grown half as long again explaining the backend selector.
The enum existed so a caller could hold either backend in one variable.
That is a caller's problem, and a small one: sinowisp, the only consumer
that swaps at run time, touches five methods, while the enum had to
forward the whole public surface.

Backends are types now. A backend this build lacks cannot be named, so
choosing wrongly is a compile error rather than a run-time Unsupported,
and is_available/available/default/Unsupported go away with the enum
they served. The per-OS extensions are inherent to Native, so reaching
for set_write_timeout through Nusb no longer compiles; the Results that
only ever carried "unsupported under the other backend" are gone too.

Hidra::new stays on Hidra<Native>, the way HashMap::new sits on the
RandomState impl: a default type parameter does not drive inference.
@karstx karstx changed the title feat(backend): choose native or nusb at run time, not by feature feat(backend): choose the backend by type, not by feature Sep 3, 2026
They hold raw OS handles, so the impls are opaque rather than derived.
Only the Linux ones had them; the Windows and macOS types were newly
public too and the lint is denied in CI.
The backend module docs still described the dispatch enum, down to a link
to the deleted module, and the nusb one still said "selected at run time".
The e2e README named the old enum variants and described a run-time
choice that is now the type parameter each target passes.
The example took a backend from HIDRA_BACKEND, which cost it a generic
function and a cfg-gated match arm to show something backends.rs already
shows. Nothing in the library reads the environment; the variable is a
convention, and the only place that needs it is the cynthion suite, where
run.sh drives both backends through one binary.
@karstx karstx changed the title feat(backend): choose the backend by type, not by feature feat(backend): use both backends in one build, chosen by type Sep 3, 2026
@karstx
karstx merged commit 0cf99af into master Sep 3, 2026
9 checks passed
@karstx
karstx deleted the feat/runtime-backend-selection branch September 3, 2026 19:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant