# jolt-native Native capabilities for [jolt](https://github.com/jolt-lang/jolt), one shared object per capability. A jolt library binds a `.so` through `jolt.ffi/defcfn`: it declares typed foreign functions and gets back integers, doubles and borrowed UTF-8 strings. That is the whole vocabulary. These crates are the other side of that boundary — the things worth writing in Rust because writing them again in anything else would mean writing them badly. ``` crates/jolt-abi the rules every library here keeps at its edge crates/jolt-moq freeq's AV media plane — MoQ over QUIC, Opus, H.264, capture ``` ## Why a workspace and not one library with features Because cargo features are additive *within* a library. A single `libjoltnative.so` whose `graphics` and `moq` features selected the parts would link egui **and** QUIC, Opus, H.264, cpal and V4L2 into one object, so a chat client that is not in a call would carry the whole media plane in order to open a window. The feature unification that comes with it has bitten this code before: enabling eframe's `wayland`/`x11` features for a desktop build pulled them into the Android one and made the activity exit on launch. A workspace keeps the parts of a monorepo worth having — one CI, one release, shared conventions in `jolt-abi` — while each consumer links only what it asked for. jolt's `:jolt/native` is a vector; naming two objects there is no harder than naming one. ## Building ```bash just build # release .so files in target/release just test # the whole workspace just libdir # where to point LD_LIBRARY_PATH ``` Everything is compiled by `zig cc`, which brings its own glibc sysroot and Linux headers, so **no system C or C++ toolchain is needed** — this repo builds on a machine with no `cc` on its PATH at all. The pinned tools come from DotSlash files in `scripts/`, fetched on first use; `dotslash` must be on PATH. Two things sit outside that promise and are worth knowing about: * **bindgen runs libclang directly** and does not inherit zig's header search path, so `v4l2r` cannot find `linux/videodev2.h`. The justfile points it at zig's bundled copies. That is what `scripts/zig-include` is for, and why building through `just` rather than bare `cargo` is the supported path. * **PipeWire** is a real system dependency — `libspa-sys` wants `pkg-config` and PipeWire's headers. It is behind the default-on `pipewire` feature of `jolt-moq`; `just test-minimal` builds without it. Turning it off costs device *names*: only ALSA PCMs remain visible ("pipewire", "sysdefault") rather than the real sources a browser would offer, which makes picking a microphone guesswork. ## jolt-moq freeq's audio and video calls, extracted from [sleek](https://github.com/codegod100/sleek), which reached them by calling Rust from Rust. A freeq call has two halves, and only one of them is here. **Signaling stayed behind.** A call is opened, joined and left over IRC TAGMSGs — `+freeq.at/av-start` and its siblings — and the server broadcasts `+freeq.at/av-state` back. Any client that speaks IRC already has everything it needs for that, in whatever language it speaks IRC in; crossing an FFI boundary to send a TAGMSG would be worse than not crossing one. This library begins once the session id and the SFU token are known. **The media plane came across**, because it is not a thing to write twice: MoQ over QUIC, Opus, H.264, camera capture and the SFU's own dial rules come to some three thousand lines that would say the same thing again in another language and be wrong in different places. `crates/jolt-moq/include/joltmoq.h` is the surface, and says more about each call than this does. The shape of it: * **Nothing calls back.** Status is drained with `joltmoq_poll_status`, video with `joltmoq_frame_poll`, both from whatever thread the caller paints on. A callback into a foreign runtime from a tokio worker is a rule about threads that the caller has to keep and that nothing can check. * **Frames are borrowed, not copied.** `joltmoq_frame_rgba` points into the decoder's own buffer until the next poll. A frame is a megabyte or two; copying it out so the caller can hand it straight to a texture upload would be two copies a frame for nothing. * **One call at a time.** There is one microphone, so there is one session. ### Its relationship to sleek `av.rs`, `av_media.rs` and `v4l2cam.rs` came from sleek's `android/src`. They are kept close to their originals so changes can still be moved between the two by eye. What was dropped on the way was the signaling-and-presentation half — `ChannelCall`, `LocalCall`, `apply_av_state`, `av_state_message` — which is sleek's own app state and belongs to a client, not to a media plane. Dropping it also dropped the `freeq-sdk` dependency entirely. Sleek still has its own copy. Pointing it at this crate instead is the obvious next step and has not been taken yet; until it is, a fix made in one place needs making in the other. ## Adding a crate One crate, one `.so`, one capability. Depend on `jolt-abi` and keep its three rules — catch panics at every entry point, lend strings from a `Scratch` rather than handing out memory to free, and let the caller ask rather than calling it back. A consumer then names your object in `:jolt/native` alongside whichever others it wants, and pays for nothing else.