cosmicnim
libcosmic behind a small C ABI, driven from Nim.
libcosmic's API is generics, traits and closures, so it has no C ABI of its own —
building libcosmic itself as a cdylib exports nothing. The cosmic_ffi crate
therefore defines the C surface: it depends on libcosmic as a normal Rust
crate and exports a handful of functions.
cosmic_ffi/ Rust cdylib -> libcosmic_ffi.so + cosmic_ffi.h
nim/ the Nim binding (cosmicnim.nim), and where the .so lands
examples/ calculator.nim, a worked example
tests/ headless tests of the example's logic
cosmicnim.nimble package metadata and the fetchLib task
justfile build / run / fetch tasks
The contract
The host owns the application state and the shape of the window. Once per
frame libcosmic calls back through on_view with an opaque builder, and the
host describes its widget tree by calling the builder functions. Widget memory
never crosses the boundary — the host only ever issues calls, and the builder
handle is dead the moment on_view returns.
Interaction runs the other way. A button carries an id the host chose; pressing
it calls on_press with that id, the host mutates its own state, and the next
on_view reflects it.
typedef void (*cosmic_on_view)(void *ctx, CosmicBuilder *b, float w, float h);
typedef void (*cosmic_on_press)(void *ctx, int32_t id);
typedef void (*cosmic_on_key)(void *ctx, const char *key);
uint32_t cosmic_abi_version(void);
int32_t cosmic_run(const CosmicConfig *config); /* blocks until closed */
on_view is handed the space actually available, which is not the size the
config asked for once a tiling compositor has had its say. on_key reports
the typed character for ordinary keys and the key's name otherwise.
ABI version
Check cosmic_abi_version before cosmic_run; the Nim binding does it for
you and refuses to run on a mismatch. A changed callback signature leaves
every symbol resolvable, so without the check a stale library reads its
arguments from whatever is in the registers — which looks like a layout bug,
not a version problem.
Because the tree is rebuilt every frame, it can depend on state: a button
disappears, or goes inert, simply by not being described that way this time
round. See cosmic_ffi.h for the full list of containers, leaves and
attributes.
cosmic_run must be called from the main thread, and ctx is only ever
touched from that thread.
From Nim
Containers are block templates, so the source has the same shape as the window:
proc onView(ctx: pointer; b: Builder) {.cdecl.} =
let c = cast[ptr Counter](ctx)
b.container:
b.fill(); b.alignCenter(); b.spacing(space(SpaceM))
b.text($c.value, TextTitle1)
b.row:
b.spacing(space(SpaceS))
b.button("−", IdDec)
b.button("Reset", IdReset, ButtonDestructive, enabled = c.value != 0)
b.button("+", IdInc, ButtonSuggested)
Running it
The demo needs nim/libcosmic_ffi.so. Take it from a release:
just fetch # or: nimble fetchLib
just run # the calculator
just test # its arithmetic, headless — no window, no cdylib call
The package version is the ABI version: cosmicnim 0.2.0 fetches the
v0.2.0 release and no other, so a stale library cannot load against newer
bindings. just fetch v0.1.2 (or COSMICNIM_TAG=v0.1.2 nimble fetchLib)
overrides that when you want it.
Signal handling
Compile anything that loads this library with -d:noSignalHandler
(examples/nim.cfg does it for the demo, and the binding warns if you
forget).
libcosmic creates threads Nim knows nothing about — tokio workers and a
notify-rs inotify watcher. Nim installs its signal handlers process-wide,
and the handler allocates a string to build a traceback, so a signal
delivered to one of those threads faults inside the handler. The crash it
produces points at rawAlloc, which tells you nothing about the real
cause; the handler has already destroyed that evidence.
As a dependency
nimble install # fetches the cdylib too, and ships it with the package
The binding resolves the library by absolute path at compile time, so an
installed package finds its .so in the nimble store without any rpath or
LD_LIBRARY_PATH fiddling. Override with -d:libCosmicFfi=/some/path.so.
or build it yourself — the cold build is long, which is what the release CI is
for:
just build
just run
just check type-checks the Nim without needing the library at all, since the
binding loads it lazily.
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 |
|