- Tcl 67.7%
- C 14.2%
- Rust 11.5%
- Makefile 2.4%
- Python 1.8%
- Other 2.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| docker | ||
| docs | ||
| embed | ||
| examples | ||
| ext | ||
| platforms | ||
| schema | ||
| tacky | ||
| tacky-core | ||
| tacky-crypto | ||
| tacky-keyring | ||
| tacky-media | ||
| tacky-omemo | ||
| tests | ||
| tools | ||
| .gitignore | ||
| .gitmodules | ||
| coverage.mk | ||
| deps.mk | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| release.mk | ||
| ROBOTS.md | ||
| tcl.mk | ||
libtacky
An XMPP client library in Tcl 9, Rust and C. The protocol, the local store, end-to-end encryption, calls and file transfer live here, behind one JSON wire API that apps speak, and as Tcl packages that programs and gateways load directly. No client of libtacky speaks XMPP itself.
Version 1.0.0, beta: the wire API is frozen but may still change before
1.0.0 proper. Licence GPL-3.0 (LICENSE).
What you get
- A backend for apps.
dist/libtacky.ais the whole library with its Tcl interpreter inside, driven through three C functions (embed/tacky.h) with JSON requests, replies and events.dist/tackyd-jsonis the same backend over stdin and stdout, for a host that would rather spawn a process than link.platforms/wasm.mkbuilds it for a browser worker. The contract isschema/tacky.schema.json; the handbook generated from it (make handbook,dist/HANDBOOK.md) documents every module, method, event and type, and the hosts' bindings are generated from the same file. - Packages for Tcl programs. The
tacky::*packages undertacky/load without the client's apps:tacky::xmpp::sessionandtacky::xmpp::client;tacky::xmpp::componentfor an XEP-0114 external component, which is how a gateway or a service bot is written;tacky::corefor the stream, XML, JSON andtacky::core::net(the socket dial and TLS);tacky::httpfor HTTP uploads and downloads over them.dist/tackydruns such a program on a machine with no Tcl, either a script or themain.tclof a zip appended to a copy of it. The handbook's Tcl reference lists every public command. - The native parts as crates with C ABIs:
tacky-core(the stream core, Rust),tacky-crypto,tacky-keyringandtacky-omemo(Rust), andtacky-media(C and C++) for calls.
Architecture
app or tackyd-json over libtacky.a; a browser worker over the wasm build
│ JSON ["module","method",{args},token] → ["result"|"error",token,…] ←
▼
embed/tacky.c one backend thread, one Tcl interpreter
▼
tacky/core/wire dispatch through the schema's allow-list; replies and events
▼
tacky/xmpp/client the client: one module per area (account, roster, message,
│ muc, omemo, calls, groupcall, file, …)
├── tacky/xmpp/session the XMPP session, in Tcl
│ └── tacky::core tacky-core (Rust): resolve, connect, TLS, XML, JSON
├── tacky/core/storage one SQLCipher store per account (ext/sqlite3)
├── tacky::core::keyring tacky-keyring (Rust): the key vault
├── tacky::omemo tacky-omemo (Rust): the OMEMO ratchet
├── tacky/http HTTP/1.1 in Tcl over tacky::core::net: its
│ socket, its TLS (pluggable) for https
└── tacky::media calls: webrtc | datachannel | host | remote
tacky/xmpp/component an XEP-0114 component over the same session,
with no client loaded
The session is Tcl, the stream is Rust. tacky-core owns one connection per handle: it resolves the domain (SRV, XEP-0156 host-meta), walks the candidates (STARTTLS, direct TLS, WebSocket), verifies the certificate (rustls, the system roots plus the user's pins and exceptions) and turns bytes into elements and back under bounds, handing them to Tcl as plain C structs. Everything an element means is Tcl: stream negotiation, SASL and SASL2, Bind 2, FAST, SCRAM with channel binding, stream management with resume, CSI, liveness and reconnects, and above the session every feature of a client.
One contract. The schema lists every module, method, argument, reply, event and type. A method not in the schema does not exist; the dispatch allow-list, the handbook and the hosts' bindings are generated from it.
The store. Each account has its own SQLCipher database, unlocked by a data key sealed in the keyring with Argon2id. A stanza is acknowledged to the server only after it is stored.
HTTP. Every HTTP exchange is the library's own: tacky/http, a Tcl
library speaking HTTP/1.1 over tacky::core::net (its socket, and its TLS
with the connection's roots and the user's pins, pluggable). Plain http
goes only to this machine, https never downgrades, and any number of
uploads and downloads run side by side without stalling the event loop.
Apps reach it through the http module, Tcl programs through
tacky::http::get, post and put.
Media. Calls are Jingle in Tcl; the media engine is one of tacky-media's
two implementations of one C API, libwebrtc (webrtc, the default) or
libdatachannel with the rtc-ma and rtc-mv engines (datachannel), or the
host's own (host, remote). A build carries one native backend. Group
calls are XEP-0272 Muji: a room and a mesh of one-to-one legs.
Trust. A peer's from, delay stamps, hashes and ids are checked before
they are believed; a certificate is accepted only through verification, a
pin or a time-limited exception; secrets never reach a log.
Boundaries. Three languages, each where it earns its place, and a rule for every crossing.
| layer | language | why there |
|---|---|---|
| the stream core, the socket dial and TLS, the XML and JSON parsers, the keyring, the crypto primitives, the OMEMO ratchet | Rust | these are the parts that read bytes from strangers and hold keys: parsers under bounds, rustls instead of OpenSSL, Argon2id, X3DH and the Double Ratchet, all memory-safe and written once for desktop, mobile and the browser (the same crates build as static libraries and as wasm) |
the Tcl bindings in ext/, the media engines in tacky-media, the host entry in embed/, SQLCipher |
C and C++ | Tcl's extension API is C, so each native part has one thin binding that turns its ABI into commands, channels and events; libwebrtc and libdatachannel are C++ and C and real-time media wants no interpreter in the path; tacky.h is what a host links |
| the session, the client, the component, the wire, the store's logic, HTTP | Tcl | everything that means something is a state machine over elements and dicts, where an event loop, introspection and fast tests pay; one script tree serves every host, and 4,700 tests run it without a server |
The crossings are narrow and all alike. A crate exports a C ABI
(tacky_core.h, tacky_ffi.h): plain C structs and byte views, no JSON,
no text parsed twice, and nothing under a crate knows Tcl
(check-native-neutral enforces it). A binding in ext/ wraps that ABI
once: a stream or a keyring call becomes a Tcl command, TLS becomes a
stacked channel on any Tcl channel, a connected socket becomes an ordinary
Tcl channel, and anything that completes later arrives as an event source
on the Tcl event loop, polled, never a callback from a foreign thread. The
Tcl packages (tacky::core, tacky::crypto, tacky::omemo,
tacky::media::rtc) are those commands, and the rest of tacky/ is Tcl
over them. Above it all sits one more crossing, the wire: a host, in any
language, speaks JSON frames to embed/tacky.c or to tackyd, and the
browser worker speaks the same frames to the wasm build. So a host never
links a crate directly, a crate never sees an interpreter, and the schema
is the only contract a host has to know.
| component | language | what it is | code | tests | coverage |
|---|---|---|---|---|---|
| core | Tcl | tacky/core: tacky::core::wire, the wire, a host's calls dispatched through the schema's allow-list, replies, events; tacky::core::storage, the store, taco_store over the sqlite3 package, keyed with the key the keyring wraps; tacky::core::keyring, the typed layer over the vault: the .keys file, its slots and autolock, with dicts in and out |
1,817 | 1,601 | 98.9 % procs |
| C | ext/core: tacky::core, the binding of tacky-core: the stream, the TLS channel, the XML fragments crossing as node dicts, json over serde_json, and the keyring's native calls and the profile lock; ext/common.h, shared by the bindings; tacky-crypto/sqlcipher: the SQLCipher provider on tacky-crypto (the sqlite3 package itself is the SQLCipher amalgamation, built and not counted) |
3,768 | 3,134 | 95.5 % lines | |
| Rust | tacky-core: the stream core: SRV and host-meta, TCP, STARTTLS, direct TLS and WebSocket over rustls, the certificate verdicts, the bounded XML stream; sans-io TLS and the C ABI, which also carries the keyring's; tacky-keyring: the key vault: Argon2id, XChaCha20-Poly1305 |
10,723 | 6,624 | 97.8 % lines | |
| crypto | C | ext/crypto: the tacky::crypto binding: the push envelope, boxes, AEAD, hashcash, digests, HMAC, PBKDF2 |
531 | 407 | 97.0 % lines |
| Rust | tacky-crypto: SHA-1/2/3, HMAC, HKDF, PBKDF2, AES (CBC, CTR, GCM) and X25519 over the RustCrypto crates, with a C ABI |
543 | 1,375 | 91.5 % lines | |
| http | Tcl | tacky/http: tacky::http, HTTP/1.1 for uploads and downloads over tacky::core::net's socket and a pluggable TLS wrap (rustls by default): handles, transfers side by side on the event loop, redirects, the loopback rule for plain http, the one-call verbs |
1,033 | 2,056 | 100.0 % procs |
| media | Tcl | tacky/media: the tacky::media contract calls run on, and its backends: the native adapter over ext/media (webrtc or datachannel, one per build), host and remote |
996 | 1,895 | 96.3 % procs |
| C | ext/media: tacky::media::rtc over tacky-media's C ABI and tacky::media::view, the Tk viewer of a frame stream; tacky-media: libtackymedia, the media library's C API (tacky_media.h) over libdatachannel, rtc-ma (miniaudio devices, Opus, the jitter buffer, RTP) and rtc-mv (V4L2 and test cameras, VP8 over libvpx, RTP, frame streams) |
9,409 | 10,318 | -- | |
| C++ | tacky-media/webrtc: libtackymedia_webrtc, the same C API over libwebrtc, with its frame stream writer |
3,714 | 271 | -- | |
| omemo | C | ext/omemo: the tacky::omemo binding of the ratchet; its Tcl side, omemo.tcl, is the client's |
858 | 371 | 98.3 % lines |
| Rust | tacky-omemo: the OMEMO ratchet: X3DH, the Double Ratchet, XEdDSA, the wire framing and payload cipher of OMEMO 0.3 and OMEMO 2, under one identity, with a C ABI |
1,767 | 3,774 | 97.2 % lines | |
| xmpp | Tcl | tacky/xmpp: JIDs (RFC 7622), the stanza node, xsearch, data forms and XEP-0420's envelope; session, the XMPP session over the stream core (SASL2, Bind 2, FAST, SCRAM, stream management, CSI, liveness and reconnects, the XML console); client, one module per area (accounts, roster, messages, MUC, OMEMO, calls, files); component, the XEP-0114 host with its iq router and the XEP-0030 and XEP-0050 responders; and tests/driver, backend driven in process |
48,569 | 85,963 | 99.7 % procs |
| everything else | C, Tcl, Python, sh, make, CMake | embed/ (the host side: tacky.h, tackyd, the image builder, the browser notifier), examples/, tools/, the test runner and server scripts, the build files (tacky-media's among them); not in the total |
9,450 | 3,521 | -- |
| total (without everything else) | Tcl 52,415 · C/C++ 18,280 · Rust 13,033 | 83,728 | 117,789 | Tcl 99.6 % procs · Rust 97.3 % lines · C 96.1 % lines |
One row per component and language, the component named on its first row. code: lines of code, without blank lines, comments and generated files; the total counts the components alone, by language in its second column, and leaves the everything else row out. tests: the lines of test code that exercise that part (tools/stats.py's COMPONENTS and TEST_RULES say which go where; an extension's Tcl suites count as tests of its C). The total and the summary count the library alone, its Tcl, C, C++ and Rust and their tests; the everything else row is shown but not counted. coverage: Tcl the procedures of tacky/ some suite called, Rust and C the lines run, from the make coverage-all run on 2026-10-07 at commit 4201d46c.
The architecture in detail, the decisions behind it and the rules for
agents working here are in ROBOTS.md. The handbook's prose is
under docs/.
Features
Every specification the library touches has a row in
docs/features.md, with what is implemented, what is
missing and a status. The same table is a part of the handbook and is what
schema/tacky.doap (XEP-0453) is generated from.
| status | meaning | specifications |
|---|---|---|
| 🔵 MOSTLY | implemented and its tests pass; not yet verified by hand against real servers and clients | 110 |
| 🟡 WIP | implemented but not finished, or not fully covered by tests | 19 |
| 🔴 TODO | not implemented; at most reserved in the API | 33 |
| 🟢 DONE | verified and stabilised, set by a maintainer by hand | 0 |
Two things worth knowing before the table: OMEMO speaks both deployed versions, OMEMO 0.3 and OMEMO 2 (with XEP-0420), under one identity, each device written under the newest it speaks and a mixed chat getting both elements; and the rows marked "reserved in the API" are methods that exist in the schema and answer "not implemented", so a host can be written against them today.
Status and tests
Beta: the API is settling, though breaking changes may still be made. Every layer is tested against
in-process peers: a fake server with real SCRAM, TLS and WebSocket on
loopback, a scripted stream relay, the fault shims of the C bindings, and
the picomemo oracle and libsignal's vectors for the ratchet. The
integration suites (tests/integration) run the client against a real
server in a container, with independent OMEMO echo bots beside it (python-omemo:
one OMEMO 0.3 only, one OMEMO 2 only, one speaking both).
The two rigs run side by side on their own ports; on the tree tagged
v1.0.0-rc2:
| server | passed | failed | skipped |
|---|---|---|---|
| Prosody 13 | 166 | 0 | 19 (notProsody: MUC/Sub, ejabberd's registration challenges and OAuth page; separateBind: neither rig server binds separately; the CSI case Prosody does not offer) |
| ejabberd 26.09 | 170 | 0 | 15 (notEjabberd: Prosody's OAuth, XEP-0494, a shared-nick case; separateBind; the no-CSI case) |
Nothing has yet been verified by hand against public servers or other clients, which is why no feature row is DONE.
83,728 lines of code and 117,789 lines of tests (the table under Architecture), 6,865 of them the integration suites run against a real server; 7,236 test cases defined (below). Coverage: Tcl 99.6 % procs · Rust 97.3 % lines · C 96.1 % lines (the make coverage-all run on 2026-10-07 at commit 4201d46c).
The figures here and under Architecture are generated by tools/stats.py
(make docs); the coverage ones come from the last make coverage-all,
whose full report is _build/cov/REPORT.md.
| suite family | files | cases |
|---|---|---|
| Tacky unit suites (tests/client, tests/http, tests/tcl) | 187 | 5384 |
| Tacky integration suites (tests/integration, need a server) | 27 | 183 |
| Rust: tacky-core | 48 | 341 |
| Rust: tacky-keyring | 5 | 46 |
| Rust: tacky-crypto | 9 | 64 |
| Rust: tacky-omemo | 14 | 166 |
| Tcl extensions (ext/*) | 19 | 1008 |
| C: tacky-media: libtackymedia, rtc-ma, rtc-mv (ctest programs) | 41 | 41 |
| C++: tacky-media: webrtc (ctest programs) | 3 | 3 |
| total | 353 | 7236 |
Building
Everything builds and tests in libtacky's container image; the machine
needs only git and docker or podman. Every make target on the host runs
inside the image.
git clone --recurse-submodules ssh://git@dev.another.im:3022/another-im/libtacky.git
cd libtacky
make all test # build everything, run every suite that needs no server
make all test REQUIRE=1 # the gate: a missing prerequisite fails instead of skipping
MEDIA_BACKEND picks the one native media backend of a build: webrtc
(the default, a pinned prebuilt libwebrtc), datachannel (libdatachannel
with Opus and VP8 in process, the submodule under tacky-media/third_party
checked out) or none.
| target | what it does |
|---|---|
make, make dist |
build everything; dist/libtacky.a |
make tackyd-json |
dist/tackyd-json, the backend behind length-prefixed JSON on stdin and stdout |
make tackyd |
dist/tackyd, the interpreter running a Tcl program on the tacky:: packages |
make test |
every suite that needs no server: Rust, the bindings, the Tcl suites, test-build, check-api |
make test-tacky-prosody, make test-tacky-ejabberd |
the integration suites against a server the target starts in a container |
make coverage-all |
the whole-tree coverage run; make stats then writes the README's numbers |
make docs |
the handbook, the README's generated sections, the DOAP file, the Rust API docs |
make host-web, make host-swift |
what the browser and Apple hosts build against |
make help |
every target, and where each one runs |
Using it
An app links embed/tacky.h and speaks JSON:
tacky *tacky_create(const char *const *backend_args, tacky_emit_fn emit, void *ud);
void tacky_send(tacky *t, const char *json, size_t len); /* ["module","method",{args},token] */
void tacky_destroy(tacky *t); /* replies and events arrive at emit */
A Tcl program loads the packages, from a built checkout (tacky/ and the
built extensions under _build/ext/* on auto_path) or through
dist/tackyd, and needs nothing of the apps' side:
package require tacky::xmpp::component
set h [tacky::xmpp::component new -domain gateway.example.org -secret $secret \
-host 127.0.0.1 -port 5347 -command onEvent]
tacky::xmpp::component connect $h
The handbook (make handbook) has the wire API and the Tcl reference;
examples/component/echo.tcl is a complete small component; ROBOTS.md is
the map of the tree.