nandi/jolt-nativepublic Fork 0
384390d0e216cb6f0e9be8e597b9d16e7a64bb29
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Paint the same tree into a terminal, for the machines with no window a7f6202 · on 384390d0e216cb6f0e9be8e597b9d16e7a64bb29 · nandi · 17d ago
README.md · 150 lines · 7.3 KBmarkdown
Blame HistoryOpen raw

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-tui     the same tree ABI, painted into a terminal → libjolttui.so
crates/jolt-moq     freeq's AV media plane   → libjoltmoq.so
jolt/glimmer-vidya  the jolt side of libvidya: glimmer's backend

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

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, 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.

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
150
# 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
jolt/glimmer-vidya  the jolt side of libvidya: glimmer's backend
```

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

```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`, `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](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.