# 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-vidya the retained-tree C ABI → libvidya.so crates/vidya-core the egui semantic layer behind it (theme, widgets, fonts) crates/jolt-tui the same tree ABI, painted into a terminal → libjolttui.so crates/jolt-moq freeq's AV media plane → libjoltmoq.so glimmer-backends/glimmer-vidya the jolt side of libvidya: glimmer's backend jvui no .so at all: dvui's shape, in jolt, on SDL3 glimmer-backends/glimmer-jvui glimmer's backend over jvui ``` All three objects land in one `target/release`, so a consumer points `LD_LIBRARY_PATH` at one directory and names in `:jolt/native` only the ones it actually wants. ## 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 The flake is the toolchain. `nix develop` is where the compiler, the C toolchain, PipeWire's headers and the kernel headers bindgen wants come from, so **nothing needs to be installed** beyond nix itself — no `cc` on PATH, no rustup, no NDK. ```bash nix develop # then: just build # release .so files in target/release just test # the whole workspace just libdir # where to point LD_LIBRARY_PATH ``` `just build` is the edit loop; the nix outputs are the artifact, built against a pinned compiler and a pinned crate graph: ```bash nix build # all three objects → result/lib nix build .#android # both device objects → result/lib/arm64-v8a nix flake check # fmt, clippy, tests, all three objects ``` Two things are worth knowing about: * **bindgen runs libclang directly**, so `v4l2r` needs to be pointed at `linux/videodev2.h`. The flake sets `V4L2R_VIDEODEV2_H_PATH` and `BINDGEN_EXTRA_CLANG_ARGS` from nixpkgs' `linuxHeaders` in both the devShell and the build, which is why building inside `nix develop` rather than a bare shell is the supported path. * **PipeWire** is a real system dependency — `libspa-sys` wants `pkg-config` and PipeWire's headers, and the flake supplies both. It is behind the default-on `pipewire` feature of `jolt-moq`; `just test-minimal` builds without it, for a machine outside the flake. 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. ### Android ```bash just ffi-android # nix build .#android ``` Same sources and same `Cargo.lock` as the desktop objects; only the target configuration moves. The NDK comes from nixpkgs, so this needs nothing installed either. The camera wants two things from the APK that the build cannot supply: a `CameraCapture` class in its `classes.dex`, and a call to `joltmoq_android_init` — see `android/jolt_main.c`. ## 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`, `v4l2cam.rs`, `android_camera.rs` and `nv12_orient.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. `android_camera.rs` is the one that did not come across unchanged. Sleek is a single shared object, so its Camera2 bridge reads the `JavaVM` and the Activity straight out of the `AndroidApp` that android-activity handed it. Here the glue is in `libvidya.so` and the media plane is not, so the handles arrive through `joltmoq_android_init` instead — see `android_jni.rs` for why the usual bridge, `ndk_context`, cannot cross two cdylibs. Everything below that line is sleek's. ## vidya The theme layer, the widget set and the retained-tree ABI that glimmer paints through. Imported from its own repo rather than depended on: egui is the only backend anyone wants now, so the indirection that let `libvidya` be swapped for a raylib or cimgui build of the same symbols was paying for a choice nobody was going to make. `crates/vidya-core` is the egui layer — and the 4MB under its `assets/` is not incidental: the Twemoji pack and the symbols font are `include_bytes!`d into the library, because a shared object has no host app to install a font set for it. `glimmer-backends/glimmer-vidya` is the other side of that ABI, and is jolt rather than Rust. A consumer takes it as a `:local/root` dep and `libvidya.so` on the loader path — frq's `deps.edn` is the worked example. ## 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. ## License MIT — the same as the projects this code came from: `crates/vidya-*` and `glimmer-backends/glimmer-vidya` from [vidya](https://tangled.org/nandi.uk/vidya), `crates/jolt-moq`'s media plane from [sleek](https://github.com/codegod100/sleek). `third-party/cpal` is a vendored pin of [cpal](https://github.com/RustAudio/cpal) and keeps its own licence.