jolt-native
Native capabilities for 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-moq freeq's AV media plane → libjoltmoq.so
jolt/glimmer-vidya the jolt side of libvidya: glimmer's backend
Both 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
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, sov4l2rcannot findlinux/videodev2.h. The justfile points it at
zig's bundled copies. That is whatscripts/zig-includeis for, and why
building throughjustrather than barecargois the supported path. - PipeWire is a real system dependency —
libspa-syswantspkg-config
and PipeWire's headers. It is behind the default-onpipewirefeature of
jolt-moq;just test-minimalbuilds 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, 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
withjoltmoq_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_rgbapoints 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.
jolt/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
jolt/glimmer-vidya from vidya,
crates/jolt-moq's media plane from
sleek. third-party/cpal is a vendored
pin of cpal and keeps its own licence.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
|