refactor: remove client-ffi and legacy nim bindings (#133)

closes: #77

The C consumer story lives downstream now: logos-chat-module wraps the
client crate and exposes its own C API. The in-tree client-ffi crate has
no consumers left, and the nim bindings still target the removed
Context-based C API.

- delete crates/client-ffi (including the message-exchange C example)
  and nim-bindings
- drop core/conversations' unused safer-ffi dependency plus the leftover
  C artifact crate-types: staticlib on core/conversations, cdylib on
  double-ratchets (neither crate has extern "C" exports)
- flake.nix: drop the default package (it built libclient_ffi.a plus its
  header); keep the logos-delivery package and the dev shell
- ci.yml: drop the C FFI smoketest steps (valgrind included), the rustup
  install the smoketest no longer needs, and the nix-build job that
  built the removed default package
- ADR 0001: point the FFI-compatibility driver at the downstream C API
  boundary instead of crates/client-ffi
This commit is contained in:
osmaczko 2026-06-15 17:55:58 +02:00 committed by GitHub
parent 78d6b6c47a
commit 9d9a691fe3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 45 additions and 1358 deletions

View File

@ -50,7 +50,6 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- run: rustup update stable && rustup default stable
- uses: cachix/install-nix-action@v31
with:
nix_version: 2.34.6
@ -60,19 +59,6 @@ jobs:
with:
primary-key: nix-${{ runner.os }}-fixtest-${{ hashFiles('flake.nix', 'flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
- name: Install valgrind
if: runner.os == 'Linux'
run: sudo apt-get install -y valgrind
- name: Build C FFI example
run: make
working-directory: crates/client-ffi/examples/message-exchange
- name: Run C FFI smoketest
run: ./c-client
working-directory: crates/client-ffi/examples/message-exchange
- name: Run C FFI smoketest under valgrind
if: runner.os == 'Linux'
run: make valgrind
working-directory: crates/client-ffi/examples/message-exchange
- name: Build logos-delivery
# Build through a patched nixpkgs (kaichaosun/nixpkgs fix-gitfetch),
# whose nix-prefetch-git disables git background auto-maintenance so the
@ -92,24 +78,3 @@ jobs:
run: nix develop -c bash -c 'LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli'
- name: Run chat-cli smoketest
run: nix develop -c ./target/release/chat-cli --name ci-test --smoketest
nix-build:
name: Nix Build
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
with:
nix_version: 2.34.6
extra_nix_config: |
experimental-features = nix-command flakes
- uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-${{ runner.os }}-fixtest-${{ hashFiles('flake.nix', 'flake.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
# Same patched-nixpkgs override; the default package pulls in
# logos-delivery-lib, so it exercises the same nim-zlib fetch.
- run: nix build --override-input nixpkgs github:kaichaosun/nixpkgs/fix-gitfetch --print-build-logs

11
.gitignore vendored
View File

@ -22,11 +22,6 @@ target
*/.DS_Store
# Compiled binary
**/ffi_nim_example
/nim-bindings/examples/pingpong
/nim-bindings/libchat
# Temporary data folder
tmp
@ -34,9 +29,3 @@ tmp
result
.DS_Store
# Generated C headers (produced by `make` in examples/c-ffi; do not commit)
crates/client-ffi/client_ffi.h
# Compiled C FFI example binary
crates/client-ffi/examples/message-exchange/c-client

268
Cargo.lock generated
View File

@ -363,7 +363,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -372,16 +372,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "client-ffi"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"libchat",
"logos-chat",
"safer-ffi",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@ -624,7 +614,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -647,7 +637,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.117",
"syn",
]
[[package]]
@ -658,7 +648,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
dependencies = [
"darling_core",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -680,7 +670,7 @@ checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -702,7 +692,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.117",
"syn",
]
[[package]]
@ -735,7 +725,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -853,41 +843,6 @@ version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "ext-trait"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d772df1c1a777963712fb68e014235e80863d6a91a85c4e06ba2d16243a310e5"
dependencies = [
"ext-trait-proc_macros",
]
[[package]]
name = "ext-trait-proc_macros"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab7934152eaf26aa5aa9f7371408ad5af4c31357073c9e84c3b9d7f11ad639a"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "extension-traits"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a296e5a895621edf9fa8329c83aa1cb69a964643e36cf54d8d7a69b789089537"
dependencies = [
"ext-trait",
]
[[package]]
name = "extern-c"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@ -923,7 +878,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -1012,7 +967,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -1206,7 +1161,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -1588,7 +1543,7 @@ dependencies = [
"indoc",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -1603,15 +1558,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "inventory"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
dependencies = [
"rustversion",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@ -1706,7 +1652,6 @@ dependencies = [
"openmls_traits 0.5.0",
"prost",
"rand_core 0.6.4",
"safer-ffi",
"shared-traits",
"storage",
"tempfile",
@ -1874,7 +1819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338"
dependencies = [
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -2067,22 +2012,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "macro_rules_attribute"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
dependencies = [
"macro_rules_attribute-proc_macro",
"paste",
]
[[package]]
name = "macro_rules_attribute-proc_macro"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
[[package]]
name = "matchers"
version = "0.2.0"
@ -2385,7 +2314,7 @@ dependencies = [
"quote",
"rstest",
"rstest_reuse",
"syn 2.0.117",
"syn",
]
[[package]]
@ -2578,16 +2507,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
dependencies = [
"proc-macro2",
"syn 1.0.109",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
@ -2595,7 +2514,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.117",
"syn",
]
[[package]]
@ -2635,7 +2554,7 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -2667,7 +2586,7 @@ dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3018,7 +2937,7 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
"syn 2.0.117",
"syn",
"unicode-ident",
]
@ -3030,7 +2949,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [
"quote",
"rand 0.8.6",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3135,38 +3054,6 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "safer-ffi"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435fdd58b61a6f1d8545274c1dfa458e905ff68c166e65e294a0130ef5e675bd"
dependencies = [
"extern-c",
"inventory",
"libc",
"macro_rules_attribute",
"paste",
"safer_ffi-proc_macros",
"scopeguard",
"stabby",
"uninit",
"unwind_safe",
"with_builtin_macros",
]
[[package]]
name = "safer_ffi-proc_macros"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156"
dependencies = [
"macro_rules_attribute",
"prettyplease 0.1.25",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -3230,7 +3117,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3269,12 +3156,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2-const-stable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -3376,41 +3257,6 @@ dependencies = [
"der",
]
[[package]]
name = "stabby"
version = "36.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b7e94eaf470c2e76b5f15fb2fb49714471a36cc512df5ee231e62e82ec79f8"
dependencies = [
"rustversion",
"stabby-abi",
]
[[package]]
name = "stabby-abi"
version = "36.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc7a63b8276b54e51bfffe3d85da56e7906b2dcfcb29018a8ab666c06734c1a"
dependencies = [
"rustc_version",
"rustversion",
"sha2-const-stable",
"stabby-macros",
]
[[package]]
name = "stabby-macros"
version = "36.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eecb7ec5611ec93ec79d120fbe55f31bea234dc1bed1001d4a071bb688651615"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"rand 0.8.6",
"syn 1.0.109",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@ -3456,7 +3302,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3465,17 +3311,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.117"
@ -3504,7 +3339,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3537,7 +3372,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3607,7 +3442,7 @@ checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3728,7 +3563,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -3823,15 +3658,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uninit"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e130f2ed46ca5d8ec13c7ff95836827f92f5f5f37fd2b2bf16f33c408d98bb6"
dependencies = [
"extension-traits",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
@ -3848,12 +3674,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "unwind_safe"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
[[package]]
name = "url"
version = "2.5.8"
@ -3982,7 +3802,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
"wasm-bindgen-shared",
]
@ -4301,8 +4121,8 @@ dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease 0.2.37",
"syn 2.0.117",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
@ -4315,10 +4135,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease 0.2.37",
"prettyplease",
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
@ -4360,26 +4180,6 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "with_builtin_macros"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a59d55032495429b87f9d69954c6c8602e4d3f3e0a747a12dea6b0b23de685da"
dependencies = [
"with_builtin_macros-proc_macros",
]
[[package]]
name = "with_builtin_macros-proc_macros"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15bd7679c15e22924f53aee34d4e448c45b674feb6129689af88593e129f8f42"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "writeable"
version = "0.6.3"
@ -4450,7 +4250,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
"synstructure",
]
@ -4471,7 +4271,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -4491,7 +4291,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
"synstructure",
]
@ -4512,7 +4312,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]
@ -4545,7 +4345,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"syn",
]
[[package]]

View File

@ -12,7 +12,6 @@ members = [
"core/shared-traits",
"core/sqlite",
"core/storage",
"crates/client-ffi",
"crates/client",
"extensions/components",
]
@ -26,7 +25,6 @@ default-members = [
"core/shared-traits",
"core/sqlite",
"core/storage",
"crates/client-ffi",
"crates/client",
]
@ -45,7 +43,7 @@ storage = { path = "core/storage" }
blake2 = "0.10"
crossbeam-channel = "0.5"
# Panicking across FFI boundaries is UB; abort is the correct strategy for a
# C FFI library.
# Panicking across FFI boundaries is UB; chat-cli registers Rust callbacks
# that liblogosdelivery invokes, so abort instead of unwinding.
[profile.release]
panic = "abort"

View File

@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["rlib","staticlib"]
crate-type = ["rlib"]
[dependencies]
# Workspace dependencies (sorted)
@ -25,7 +25,6 @@ openmls_memory_storage = "0.5.0"
openmls_traits = "0.5.0"
prost = "0.14.1"
rand_core = { version = "0.6" }
safer-ffi = "0.1.13"
thiserror = "2.0.17"
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }

View File

@ -2,7 +2,7 @@ use std::fmt::Debug;
use crate::proto::{self, Message};
// FFI Type definitions
// Public type definitions
// This struct represents Outbound data.
// It wraps an encoded payload with a delivery address, so it can be handled by the delivery service.

View File

@ -4,7 +4,7 @@ version = "0.0.1"
edition = "2024"
[lib]
crate-type = ["rlib", "cdylib"]
crate-type = ["rlib"]
[dependencies]
# Workspace dependencies (sorted)

View File

@ -1,23 +0,0 @@
[package]
name = "client-ffi"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["staticlib", "rlib"]
[[bin]]
name = "generate-headers"
required-features = ["headers"]
[dependencies]
# Workspace dependencies (sorted)
crossbeam-channel = { workspace = true }
libchat = { workspace = true }
logos-chat = { workspace = true }
# External dependencies (sorted)
safer-ffi = "0.1.13"
[features]
headers = ["safer-ffi/headers"]

View File

@ -1,39 +0,0 @@
REPO_ROOT := $(shell cd ../../../.. && pwd)
CARGO_PROFILE ?= debug
LIB_DIR := $(REPO_ROOT)/target/$(CARGO_PROFILE)
INCLUDE_DIR := $(REPO_ROOT)/crates/client-ffi
HEADER := $(INCLUDE_DIR)/client_ffi.h
CC ?= cc
CFLAGS := -Wall -Wextra -std=c11 -I$(INCLUDE_DIR)
LIBS := -L$(LIB_DIR) -lclient_ffi -lpthread -ldl -lm
.PHONY: all run valgrind clean generate-headers _cargo
all: c-client
generate-headers:
cargo run --manifest-path $(REPO_ROOT)/Cargo.toml \
-p client-ffi --bin generate-headers --features headers \
-- $(HEADER)
_cargo:
cargo build --manifest-path $(REPO_ROOT)/Cargo.toml -p client-ffi \
$(if $(filter release,$(CARGO_PROFILE)),--release,)
c-client: src/main.c generate-headers _cargo
$(CC) $(CFLAGS) src/main.c $(LIBS) -o c-client
run: c-client
./c-client
valgrind: c-client
valgrind \
--error-exitcode=1 \
--leak-check=full \
--errors-for-leak-kinds=definite,indirect \
--track-origins=yes \
./c-client
clean:
rm -f c-client $(HEADER)

View File

@ -1,21 +0,0 @@
# message-exchange
An example C application built on top of [`crates/client-ffi`](../../).
It demonstrates that the C ABI exposed by `crates/client-ffi` is straightforward to
consume from plain C — or from any language that can call into a C ABI. No Rust code,
no Cargo project: just a C source file linked against the pre-built static library.
## Building and running
```sh
make # builds client-ffi with Cargo, then compiles src/main.c
make run # build + execute
make clean # remove the compiled binary
```
For a release build:
```sh
make CARGO_PROFILE=release
```

View File

@ -1,228 +0,0 @@
/*
* message-exchange: Saro-Raya message exchange written entirely in C.
*
* Demonstrates that the client-ffi C API is straightforward to consume
* directly no Rust glue required. Build with the provided Makefile.
*/
#include "client_ffi.h"
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* ------------------------------------------------------------------
* Convenience macros for building slice_ref_uint8_t values.
* SLICE(p, n) arbitrary pointer + length.
* STR(s) string literal (length computed at compile time).
* ------------------------------------------------------------------ */
#define SLICE(p, n) ((slice_ref_uint8_t){ .ptr = (const uint8_t *)(p), .len = (n) })
#define STR(s) SLICE(s, sizeof(s) - 1)
/* ------------------------------------------------------------------
* In-memory delivery bus (shared by all clients, like InProcessDelivery)
* ------------------------------------------------------------------ */
#define MAX_ENVELOPES 32
#define MAX_ENVELOPE_SZ 2048
typedef struct {
uint8_t data[MAX_ENVELOPE_SZ];
size_t len;
} Envelope;
typedef struct {
Envelope items[MAX_ENVELOPES];
int head;
int tail;
int count;
} Queue;
static Queue bus;
static void queue_init(Queue *q)
{
memset(q, 0, sizeof(*q));
}
static void queue_push(Queue *q, const uint8_t *data, size_t len)
{
assert(q->count < MAX_ENVELOPES && "delivery queue overflow");
assert(len <= MAX_ENVELOPE_SZ && "envelope too large");
memcpy(q->items[q->tail].data, data, len);
q->items[q->tail].len = len;
q->tail = (q->tail + 1) % MAX_ENVELOPES;
q->count++;
}
static int queue_pop(Queue *q, const uint8_t **data_out, size_t *len_out)
{
if (q->count == 0) return 0;
*data_out = q->items[q->head].data;
*len_out = q->items[q->head].len;
q->head = (q->head + 1) % MAX_ENVELOPES;
q->count--;
return 1;
}
/* ------------------------------------------------------------------
* Delivery callback: all clients share one bus.
* ------------------------------------------------------------------ */
static int32_t deliver_cb(
const uint8_t *addr_ptr, size_t addr_len,
const uint8_t *data_ptr, size_t data_len)
{
(void)addr_ptr; (void)addr_len;
queue_push(&bus, data_ptr, data_len);
return 0;
}
/* ------------------------------------------------------------------
* Helper: pop one envelope from the bus, hand it to receiver's worker,
* then wait for the worker to produce events. Returns a heap-allocated
* event list; caller frees with event_list_free().
* ------------------------------------------------------------------ */
static EventList_t *route(ClientHandle_t *receiver)
{
const uint8_t *data;
size_t len;
int ok = queue_pop(&bus, &data, &len);
assert(ok && "expected an envelope in the bus");
client_push_inbound(receiver, SLICE(data, len));
/* Block until the worker decrypts the payload and produces events. */
EventList_t *evs = client_wait_events(receiver, 5000);
assert(event_list_len(evs) > 0 && "timed out waiting for events");
return evs;
}
/* ------------------------------------------------------------------
* Helper: locate the first MessageReceived event in a list and copy
* its content into the caller-supplied buffer. Returns -1 if not found.
* ------------------------------------------------------------------ */
static int find_message(EventList_t *evs, char *out, size_t out_cap, size_t *out_len)
{
size_t n = event_list_len(evs);
for (size_t i = 0; i < n; ++i) {
if (event_list_kind_at(evs, i) == EVENT_KIND_MESSAGE_RECEIVED) {
slice_ref_uint8_t s = event_list_content_at(evs, i);
assert(s.len <= out_cap && "content buffer too small");
memcpy(out, s.ptr, s.len);
*out_len = s.len;
return (int)i;
}
}
return -1;
}
/* ------------------------------------------------------------------
* Main
* ------------------------------------------------------------------ */
int main(void)
{
queue_init(&bus);
/* Create clients — both share the same delivery bus */
ClientHandle_t *saro = client_create(STR("saro"), deliver_cb);
ClientHandle_t *raya = client_create(STR("raya"), deliver_cb);
assert(saro && "client_create returned NULL for saro");
assert(raya && "client_create returned NULL for raya");
/* Raya generates an intro bundle */
CreateIntroResult_t *raya_intro = client_create_intro_bundle(raya);
assert(create_intro_result_error_code(raya_intro) == 0);
slice_ref_uint8_t intro_bytes = create_intro_result_bytes(raya_intro);
/* Saro initiates a conversation with Raya */
CreateConvoResult_t *saro_convo = client_create_conversation(
saro, intro_bytes, STR("hello raya"));
assert(create_convo_result_error_code(saro_convo) == 0);
create_intro_result_free(raya_intro);
/* Route saro -> raya: expect [ConversationStarted, MessageReceived] */
EventList_t *evs = route(raya);
assert(event_list_len(evs) == 2 && "expected 2 events for invite");
assert(event_list_kind_at(evs, 0) == EVENT_KIND_CONVERSATION_STARTED
&& "first event should be ConversationStarted");
assert(event_list_conversation_class_at(evs, 0) == FFI_CONVERSATION_CLASS_PRIVATE
&& "expected Private convo class");
char msg[64];
size_t msg_len;
int idx = find_message(evs, msg, sizeof(msg), &msg_len);
assert(idx >= 0 && "expected MessageReceived from saro");
assert(msg_len == 10 && memcmp(msg, "hello raya", 10) == 0);
printf("Raya received: \"%.*s\"\n", (int)msg_len, msg);
/* Copy Raya's convo_id from the ConversationStarted event */
slice_ref_uint8_t cid_ref = event_list_convo_id_at(evs, 0);
uint8_t raya_cid[256];
size_t raya_cid_len = cid_ref.len;
if (raya_cid_len >= sizeof(raya_cid)) {
fprintf(stderr, "conversation id too long (%zu bytes)\n", raya_cid_len);
return 1;
}
memcpy(raya_cid, cid_ref.ptr, raya_cid_len);
event_list_free(evs);
/* Raya replies */
ErrorCode_t rc = client_send_message(
raya, SLICE(raya_cid, raya_cid_len), STR("hi saro"));
assert(rc == ERROR_CODE_NONE);
evs = route(saro);
assert(event_list_len(evs) == 1 && "expected MessageReceived only");
assert(event_list_kind_at(evs, 0) == EVENT_KIND_MESSAGE_RECEIVED);
idx = find_message(evs, msg, sizeof(msg), &msg_len);
assert(idx >= 0);
assert(msg_len == 7 && memcmp(msg, "hi saro", 7) == 0);
printf("Saro received: \"%.*s\"\n", (int)msg_len, msg);
event_list_free(evs);
/* Multiple back-and-forth rounds */
slice_ref_uint8_t saro_cid = create_convo_result_id(saro_convo);
for (int i = 0; i < 3; i++) {
char text[32];
int tlen = snprintf(text, sizeof(text), "msg %d", i);
rc = client_send_message(saro, saro_cid, SLICE(text, (size_t)tlen));
assert(rc == ERROR_CODE_NONE);
evs = route(raya);
idx = find_message(evs, msg, sizeof(msg), &msg_len);
assert(idx >= 0);
assert((int)msg_len == tlen);
assert(memcmp(msg, text, (size_t)tlen) == 0);
event_list_free(evs);
char reply[32];
int rlen = snprintf(reply, sizeof(reply), "reply %d", i);
rc = client_send_message(
raya, SLICE(raya_cid, raya_cid_len), SLICE(reply, (size_t)rlen));
assert(rc == ERROR_CODE_NONE);
evs = route(saro);
idx = find_message(evs, msg, sizeof(msg), &msg_len);
assert(idx >= 0);
assert((int)msg_len == rlen);
assert(memcmp(msg, reply, (size_t)rlen) == 0);
event_list_free(evs);
}
/* Cleanup */
create_convo_result_free(saro_convo);
client_destroy(saro);
client_destroy(raya);
printf("Message exchange complete.\n");
return 0;
}

View File

@ -1,416 +0,0 @@
use safer_ffi::prelude::*;
use crossbeam_channel::{Receiver, Sender};
use crate::delivery::{CDelivery, DeliverFn};
use libchat::ChatError;
use logos_chat::{ChatClient, ClientError, ConversationClass, Event};
// ---------------------------------------------------------------------------
// Opaque client handle
// ---------------------------------------------------------------------------
#[derive_ReprC]
#[repr(opaque)]
pub struct ClientHandle {
client: ChatClient<CDelivery>,
events: Receiver<Event>,
inbound: Sender<Vec<u8>>,
}
// ---------------------------------------------------------------------------
// Error codes
// ---------------------------------------------------------------------------
#[derive_ReprC]
#[repr(i32)]
pub enum ErrorCode {
None = 0,
BadUtf8 = -1,
/// Failure parsing or processing an introduction bundle.
BadIntro = -2,
DeliveryFail = -3,
UnknownError = -4,
/// Failure decoding, decrypting, or processing an inbound payload.
BadPayload = -5,
}
// ---------------------------------------------------------------------------
// Event taxonomy (C-side view of Event)
// ---------------------------------------------------------------------------
#[derive_ReprC]
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum EventKind {
/// Sentinel returned by `event_list_kind_at` for out-of-bounds indices.
/// Never the kind of a real event row.
Invalid = -1,
ConversationStarted = 0,
MessageReceived = 1,
}
#[derive_ReprC]
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum FfiConversationClass {
/// Sentinel for accessor calls that don't apply to the queried row
/// (out-of-bounds, or a non-`ConversationStarted` event).
Invalid = -1,
Private = 0,
Group = 1,
}
impl From<ConversationClass> for FfiConversationClass {
fn from(c: ConversationClass) -> Self {
match c {
ConversationClass::Private => FfiConversationClass::Private,
ConversationClass::Group => FfiConversationClass::Group,
}
}
}
// ---------------------------------------------------------------------------
// Result types (opaque, heap-allocated via repr_c::Box)
// ---------------------------------------------------------------------------
#[derive_ReprC]
#[repr(opaque)]
pub struct CreateIntroResult {
error_code: i32,
data: Option<Vec<u8>>,
}
#[derive_ReprC]
#[repr(opaque)]
pub struct CreateConvoResult {
error_code: i32,
convo_id: Option<String>,
}
/// An ordered list of events with a status code. Inspect `error_code` (zero
/// on success) before iterating with `event_list_len` and the indexed
/// accessors.
#[derive_ReprC]
#[repr(opaque)]
pub struct EventList {
error_code: i32,
events: Vec<EventRow>,
}
enum EventRow {
ConversationStarted {
convo_id: String,
class: FfiConversationClass,
},
MessageReceived {
convo_id: String,
content: Vec<u8>,
},
}
impl EventRow {
/// Translate an [`Event`] into the FFI row shape, or `None` for variants
/// without an FFI representation.
fn from_event(event: Event) -> Option<Self> {
match event {
Event::ConversationStarted {
convo_id, class, ..
} => Some(EventRow::ConversationStarted {
convo_id: convo_id.to_string(),
class: class.into(),
}),
Event::MessageReceived {
convo_id, content, ..
} => Some(EventRow::MessageReceived {
convo_id: convo_id.to_string(),
content,
}),
_ => None,
}
}
fn convo_id(&self) -> &str {
match self {
EventRow::ConversationStarted { convo_id, .. }
| EventRow::MessageReceived { convo_id, .. } => convo_id,
}
}
fn content(&self) -> &[u8] {
match self {
EventRow::MessageReceived { content, .. } => content,
_ => &[],
}
}
}
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
/// Create an ephemeral in-memory client. Returns NULL if `callback` is None or
/// `name` is not valid UTF-8. Free with `client_destroy`.
#[ffi_export]
fn client_create(
name: c_slice::Ref<'_, u8>,
callback: DeliverFn,
) -> Option<repr_c::Box<ClientHandle>> {
let name_str = match std::str::from_utf8(name.as_slice()) {
Ok(s) => s,
Err(_) => return None,
};
callback?;
let (inbound_tx, inbound_rx) = crossbeam_channel::unbounded();
let delivery = CDelivery::new(callback, inbound_rx);
let (client, events) = ChatClient::new(name_str, delivery);
Some(
Box::new(ClientHandle {
client,
events,
inbound: inbound_tx,
})
.into(),
)
}
/// Free a client handle. Must not be used after this call.
#[ffi_export]
fn client_destroy(handle: repr_c::Box<ClientHandle>) {
drop(handle)
}
// ---------------------------------------------------------------------------
// Identity
// ---------------------------------------------------------------------------
/// Return the installation name as an owned byte slice.
/// Free with `client_installation_name_free`.
#[ffi_export]
fn client_installation_name(handle: &ClientHandle) -> c_slice::Box<u8> {
handle
.client
.installation_name()
.as_bytes()
.to_vec()
.into_boxed_slice()
.into()
}
#[ffi_export]
fn client_installation_name_free(name: c_slice::Box<u8>) {
drop(name)
}
// ---------------------------------------------------------------------------
// Intro bundle
// ---------------------------------------------------------------------------
/// Produce a serialised introduction bundle for out-of-band sharing.
/// Free with `create_intro_result_free`.
#[ffi_export]
fn client_create_intro_bundle(handle: &mut ClientHandle) -> repr_c::Box<CreateIntroResult> {
let result = match handle.client.create_intro_bundle() {
Ok(bytes) => CreateIntroResult {
error_code: ErrorCode::None as i32,
data: Some(bytes),
},
Err(_) => CreateIntroResult {
error_code: ErrorCode::UnknownError as i32,
data: None,
},
};
Box::new(result).into()
}
#[ffi_export]
fn create_intro_result_error_code(r: &CreateIntroResult) -> i32 {
r.error_code
}
/// Returns an empty slice when error_code != 0.
/// The slice is valid only while `r` is alive.
#[ffi_export]
fn create_intro_result_bytes(r: &CreateIntroResult) -> c_slice::Ref<'_, u8> {
r.data.as_deref().unwrap_or(&[]).into()
}
#[ffi_export]
fn create_intro_result_free(r: repr_c::Box<CreateIntroResult>) {
drop(r)
}
// ---------------------------------------------------------------------------
// Create conversation
// ---------------------------------------------------------------------------
/// Parse an intro bundle and initiate a private conversation.
/// Outbound envelopes are dispatched through the delivery callback.
/// Free with `create_convo_result_free`.
#[ffi_export]
fn client_create_conversation(
handle: &mut ClientHandle,
bundle: c_slice::Ref<'_, u8>,
content: c_slice::Ref<'_, u8>,
) -> repr_c::Box<CreateConvoResult> {
let result = match handle
.client
.create_conversation(bundle.as_slice(), content.as_slice())
{
Ok(convo_id) => CreateConvoResult {
error_code: ErrorCode::None as i32,
convo_id: Some(convo_id),
},
Err(ClientError::Chat(ChatError::Delivery(_))) => CreateConvoResult {
error_code: ErrorCode::DeliveryFail as i32,
convo_id: None,
},
Err(ClientError::Chat(_)) => CreateConvoResult {
error_code: ErrorCode::BadIntro as i32,
convo_id: None,
},
};
Box::new(result).into()
}
#[ffi_export]
fn create_convo_result_error_code(r: &CreateConvoResult) -> i32 {
r.error_code
}
/// Returns an empty slice when error_code != 0.
/// The slice is valid only while `r` is alive.
#[ffi_export]
fn create_convo_result_id(r: &CreateConvoResult) -> c_slice::Ref<'_, u8> {
r.convo_id.as_deref().unwrap_or("").as_bytes().into()
}
#[ffi_export]
fn create_convo_result_free(r: repr_c::Box<CreateConvoResult>) {
drop(r)
}
// ---------------------------------------------------------------------------
// Send message
// ---------------------------------------------------------------------------
/// Encrypt `content` and dispatch outbound envelopes. Returns an `ErrorCode`.
#[ffi_export]
fn client_send_message(
handle: &mut ClientHandle,
convo_id: c_slice::Ref<'_, u8>,
content: c_slice::Ref<'_, u8>,
) -> ErrorCode {
let id_str = match std::str::from_utf8(convo_id.as_slice()) {
Ok(s) => s,
Err(_) => return ErrorCode::BadUtf8,
};
match handle.client.send_message(id_str, content.as_slice()) {
Ok(()) => ErrorCode::None,
Err(ClientError::Chat(ChatError::Delivery(_))) => ErrorCode::DeliveryFail,
Err(_) => ErrorCode::UnknownError,
}
}
// ---------------------------------------------------------------------------
// Inbound (push wire payloads in, drain events out)
// ---------------------------------------------------------------------------
/// Feed an inbound payload (read off the wire by the host) to the client's
/// worker, which decrypts it and produces events for `client_poll_events`.
#[ffi_export]
fn client_push_inbound(handle: &ClientHandle, payload: c_slice::Ref<'_, u8>) {
// Disconnected only if the worker has stopped; nothing to do then.
let _ = handle.inbound.send(payload.as_slice().to_vec());
}
/// Drain every event the worker has produced since the last call. The list may
/// be empty. Free with `event_list_free`.
#[ffi_export]
fn client_poll_events(handle: &ClientHandle) -> repr_c::Box<EventList> {
let events = handle
.events
.try_iter()
.filter_map(EventRow::from_event)
.collect();
Box::new(EventList {
error_code: ErrorCode::None as i32,
events,
})
.into()
}
/// Block until the worker produces an event or `timeout_ms` elapses, then drain
/// everything available. Parks on the channel (no busy-wait); an empty list
/// means timeout or a stopped worker. Free with `event_list_free`.
#[ffi_export]
fn client_wait_events(handle: &ClientHandle, timeout_ms: u64) -> repr_c::Box<EventList> {
let timeout = std::time::Duration::from_millis(timeout_ms);
let mut events = Vec::new();
if let Ok(first) = handle.events.recv_timeout(timeout) {
events.extend(EventRow::from_event(first));
events.extend(handle.events.try_iter().filter_map(EventRow::from_event));
}
Box::new(EventList {
error_code: ErrorCode::None as i32,
events,
})
.into()
}
#[ffi_export]
fn event_list_error_code(list: &EventList) -> i32 {
list.error_code
}
#[ffi_export]
fn event_list_len(list: &EventList) -> usize {
list.events.len()
}
/// Returns `EventKind::Invalid` for out-of-bounds indices.
#[ffi_export]
fn event_list_kind_at(list: &EventList, idx: usize) -> EventKind {
match list.events.get(idx) {
Some(EventRow::ConversationStarted { .. }) => EventKind::ConversationStarted,
Some(EventRow::MessageReceived { .. }) => EventKind::MessageReceived,
None => EventKind::Invalid,
}
}
/// Returns an empty slice for out-of-bounds indices.
/// The slice is valid only while `list` is alive.
#[ffi_export]
fn event_list_convo_id_at(list: &EventList, idx: usize) -> c_slice::Ref<'_, u8> {
list.events
.get(idx)
.map(|r| r.convo_id().as_bytes())
.unwrap_or(&[])
.into()
}
/// Returns an empty slice for non-`MessageReceived` events or out-of-bounds.
/// The slice is valid only while `list` is alive.
#[ffi_export]
fn event_list_content_at(list: &EventList, idx: usize) -> c_slice::Ref<'_, u8> {
list.events
.get(idx)
.map(EventRow::content)
.unwrap_or(&[])
.into()
}
/// Returns `FfiConversationClass::Invalid` for non-`ConversationStarted`
/// events or out-of-bounds.
#[ffi_export]
fn event_list_conversation_class_at(list: &EventList, idx: usize) -> FfiConversationClass {
match list.events.get(idx) {
Some(EventRow::ConversationStarted { class, .. }) => *class,
_ => FfiConversationClass::Invalid,
}
}
#[ffi_export]
fn event_list_free(list: repr_c::Box<EventList>) {
drop(list)
}

View File

@ -1,6 +0,0 @@
fn main() -> std::io::Result<()> {
let path = std::env::args()
.nth(1)
.unwrap_or_else(|| "client_ffi.h".into());
client_ffi::generate_headers(&path)
}

View File

@ -1,56 +0,0 @@
use crossbeam_channel::Receiver;
use libchat::AddressedEnvelope;
use logos_chat::{DeliveryService, Transport};
/// C callback invoked for each outbound envelope. Return 0 or positive on success, negative on
/// error. `addr_ptr/addr_len` is the delivery address; `data_ptr/data_len` is the encrypted
/// payload. Both pointers are borrowed for the duration of the call only; the callee must not
/// retain or free them.
pub type DeliverFn = Option<
unsafe extern "C" fn(
addr_ptr: *const u8,
addr_len: usize,
data_ptr: *const u8,
data_len: usize,
) -> i32,
>;
#[derive(Debug)]
pub struct CDelivery {
pub callback: DeliverFn,
inbound_rx: Option<Receiver<Vec<u8>>>,
}
impl CDelivery {
pub fn new(callback: DeliverFn, inbound: Receiver<Vec<u8>>) -> Self {
Self {
callback,
inbound_rx: Some(inbound),
}
}
}
impl DeliveryService for CDelivery {
type Error = i32;
fn publish(&mut self, envelope: AddressedEnvelope) -> Result<(), i32> {
let cb = self.callback.expect("callback must be non-null");
let addr = envelope.delivery_address.as_bytes();
let data = envelope.data.as_slice();
let rc = unsafe { cb(addr.as_ptr(), addr.len(), data.as_ptr(), data.len()) };
if rc < 0 { Err(rc) } else { Ok(()) }
}
fn subscribe(&mut self, _delivery_address: &str) -> Result<(), Self::Error> {
// TODO: (P1) CDelivery does not support delivery_address filtering
Ok(())
}
}
impl Transport for CDelivery {
fn inbound(&mut self) -> Receiver<Vec<u8>> {
self.inbound_rx
.take()
.expect("CDelivery::inbound called more than once")
}
}

View File

@ -1,7 +0,0 @@
mod api;
mod delivery;
#[cfg(feature = "headers")]
pub fn generate_headers(path: &str) -> std::io::Result<()> {
safer_ffi::headers::builder().to_file(path)?.generate()
}

View File

@ -5,7 +5,7 @@
| Status | Accepted |
| Issue | https://github.com/logos-messaging/libchat/issues/97 |
| Date | 2026-05-19 |
| Last revised | 2026-06-09 |
| Last revised | 2026-06-11 |
## Context and Problem
@ -17,7 +17,7 @@ Issue #97 captures the requirement for an observation surface that does not pigg
- **Simplicity of the core.** Fully synchronous and caller-driven: no background work, no callbacks out. External effects flow through services injected as method parameters.
- **Asynchronous delivery at the client.** Applications consume events on their own schedule. Observations from sync-triggered processing and observations from background work share a single delivery surface, so the application sees one notification stream and does not care which path produced any given event.
- **FFI compatibility.** Payloads crossing the `safer-ffi` boundary in `crates/client-ffi` are limited to owned, concrete data — no closures, generics, or non-`'static` references — so any delivery mechanism must degrade to a sync drain on that side.
- **FFI compatibility.** Downstream modules wrap the client behind a C API; payloads crossing that boundary are limited to owned, concrete data — no closures, generics, or non-`'static` references — so any delivery mechanism must degrade to a sync drain on that side.
## Architecture
@ -35,7 +35,7 @@ flowchart TB
B == "Event (async channel)" ==> A
```
Crates: **app**`bin/chat-cli`, future `logos-chat-module`; **client**`crates/client`, `crates/client-ffi`; **core**`core/conversations` and friends in libchat.
Crates: **app**`bin/chat-cli`, future `logos-chat-module`; **client**`crates/client`; **core**`core/conversations` and friends in libchat.
## Decisions

View File

@ -27,60 +27,13 @@
});
in
{
packages = forAllSystems ({ pkgs, system }:
let
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
rustPlatform = pkgs.makeRustPlatform {
cargo = rustToolchain;
rustc = rustToolchain;
};
logos-delivery-lib = logos-delivery.packages.${system}.liblogosdelivery.override {
packages = forAllSystems ({ system, ... }:
{
logos-delivery = logos-delivery.packages.${system}.liblogosdelivery.override {
enablePostgres = false;
enableNimDebugDlOpen = false;
chroniclesLogLevel = "FATAL";
};
in
{
logos-delivery = logos-delivery-lib;
default = rustPlatform.buildRustPackage {
pname = "libchat";
version = (builtins.fromTOML (builtins.readFile ./crates/client-ffi/Cargo.toml)).package.version;
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter = path: type:
let base = builtins.baseNameOf path;
in !(builtins.elem base [ "target" "nim-bindings" ".git" ".github" "tmp" ]);
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"chat-proto-0.1.0" = "sha256-hiFH/EwTpJd9RLtq1uF2CilzinedfR2o4jvqFaDhk+g=";
};
};
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake ];
buildType = "release";
doCheck = false;
cargoBuildFlags = [ "--workspace" "--exclude" "chat-cli" ];
postBuild = ''
cargo run --frozen --release --bin generate-headers --features headers -p client-ffi -- crates/client-ffi/client_ffi.h
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib $out/include
cp target/${pkgs.stdenv.hostPlatform.rust.rustcTarget}/release/libclient_ffi.a $out/lib/
cp crates/client-ffi/client_ffi.h $out/include/
runHook postInstall
'';
meta = with pkgs.lib; {
description = "Logos Chat library (C FFI)";
platforms = platforms.unix;
};
};
}
);

View File

@ -1,221 +0,0 @@
# Nim FFI bindings for libchat conversations library
# Error codes (must match Rust ErrorCode enum)
const
ErrNone* = 0'i32
ErrBadPtr* = -1'i32
ErrBadConvoId* = -2'i32
ErrBadIntro* = -3'i32
ErrNotImplemented* = -4'i32
ErrBufferExceeded* = -5'i32
ErrUnknownError* = -6'i32
# Opaque handle type for Context
type ContextHandle* = pointer
type
## Slice for passing byte arrays to safer_ffi functions
SliceUint8* = object
`ptr`*: ptr uint8
len*: csize_t
## Vector type returned by safer_ffi functions (must be freed)
VecUint8* = object
`ptr`*: ptr uint8
len*: csize_t
cap*: csize_t
## repr_c::String type from safer_ffi
ReprCString* = object
`ptr`*: ptr char
len*: csize_t
cap*: csize_t
## Payload structure for FFI (matches Rust Payload struct)
Payload* = object
address*: ReprCString
data*: VecUint8
## Vector of Payloads returned by safer_ffi functions
VecPayload* = object
`ptr`*: ptr Payload
len*: csize_t
cap*: csize_t ## Vector of Payloads returned by safer_ffi functions
VecString* = object
`ptr`*: ptr ReprCString
len*: csize_t
cap*: csize_t
## Result structure for create_intro_bundle
## error_code is 0 on success, negative on error (see ErrorCode)
CreateIntroResult* = object
error_code*: int32
intro_bytes*: VecUint8
## Result structure for send_content
## error_code is 0 on success, negative on error (see ErrorCode)
SendContentResult* = object
error_code*: int32
payloads*: VecPayload
## Result structure for handle_payload
## error_code is 0 on success, negative on error (see ErrorCode)
HandlePayloadResult* = object
error_code*: int32
convo_id*: ReprCString
content*: VecUint8
is_new_convo*: bool
## Result from create_new_private_convo
## error_code is 0 on success, negative on error (see ErrorCode)
NewConvoResult* = object
error_code*: int32
convo_id*: ReprCString
payloads*: VecPayload
## Result from list_conversations
## error_code is 0 on success, negative on error (see ErrorCode)
ListConvoResult* = object
error_code*: int32
convo_ids*: VecString
# FFI function imports
## Creates a new libchat Context
## Returns: Opaque handle to the context. Must be freed with destroy_context()
proc create_context*(name: ReprCString): ContextHandle {.importc.}
## Returns the friendly name of the context's identity
## The result must be freed by the caller (repr_c::String ownership transfers)
proc installation_name*(ctx: ContextHandle): ReprCString {.importc.}
## Destroys a context and frees its memory
## - handle must be a valid pointer from create_context()
## - handle must not be used after this call
proc destroy_context*(ctx: ContextHandle) {.importc.}
## Free a ReprCString returned by any of the FFI functions
## - s must be an owned ReprCString value returned from an FFI function
## - s must not be used after this call
proc destroy_string*(s: ReprCString) {.importc.}
## Creates an intro bundle for sharing with other users
## Returns: CreateIntroResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_intro_result()
proc create_intro_bundle*(ctx: ContextHandle): CreateIntroResult {.importc.}
## Creates a new private conversation
## Returns: NewConvoResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_convo_result()
proc create_new_private_convo*(
ctx: ContextHandle, bundle: SliceUint8, content: SliceUint8
): NewConvoResult {.importc.}
## Get the available conversation identifers.
## Returns: ListConvoResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_list_result()
proc list_conversations*(ctx: ContextHandle): ListConvoResult {.importc.}
## Sends content to an existing conversation
## Returns: SendContentResult struct - check error_code field (0 = success, negative = error)
## The result must be freed with destroy_send_content_result()
proc send_content*(
ctx: ContextHandle, convo_id: ReprCString, content: SliceUint8
): SendContentResult {.importc.}
## Handles an incoming payload
## Returns: HandlePayloadResult struct - check error_code field (0 = success, negative = error)
## This call does not always generate content. If content is zero bytes long then there
## is no data, and the convo_id should be ignored.
## The result must be freed with destroy_handle_payload_result()
proc handle_payload*(
ctx: ContextHandle, payload: SliceUint8
): HandlePayloadResult {.importc.}
## Free the result from create_intro_bundle
proc destroy_intro_result*(result: CreateIntroResult) {.importc.}
## Free the result from create_new_private_convo
proc destroy_convo_result*(result: NewConvoResult) {.importc.}
## Free the result from list_conversation
proc destroy_list_result*(result: ListConvoResult) {.importc.}
## Free the result from send_content
proc destroy_send_content_result*(result: SendContentResult) {.importc.}
## Free the result from handle_payload
proc destroy_handle_payload_result*(result: HandlePayloadResult) {.importc.}
# ============================================================================
# Helper functions
# ============================================================================
## Create a SliceRefUint8 from a string
proc toSlice*(s: string): SliceUint8 =
if s.len == 0:
SliceUint8(`ptr`: nil, len: 0)
else:
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
## Create a SliceRefUint8 from a seq[byte]
proc toSlice*(s: seq[byte]): SliceUint8 =
if s.len == 0:
SliceUint8(`ptr`: nil, len: 0)
else:
SliceUint8(`ptr`: cast[ptr uint8](unsafeAddr s[0]), len: csize_t(s.len))
## Convert a ReprCString to a Nim string
proc `$`*(s: ReprCString): string =
if s.ptr == nil or s.len == 0:
return ""
result = newString(s.len)
copyMem(addr result[0], s.ptr, s.len)
## Create a ReprCString from a Nim string for passing to FFI functions.
## WARNING: The returned ReprCString borrows from the input string.
## The input string must remain valid for the duration of the FFI call.
## cap is set to 0 to prevent Rust from attempting to deallocate Nim memory.
proc toReprCString*(s: string): ReprCString =
if s.len == 0:
ReprCString(`ptr`: nil, len: 0, cap: 0)
else:
ReprCString(`ptr`: cast[ptr char](unsafeAddr s[0]), len: csize_t(s.len), cap: 0)
## Convert a VecUint8 to a seq[string]
proc toSeq*(v: VecString): seq[string] =
if v.ptr == nil or v.len == 0:
return @[]
result = newSeq[string](v.len)
let arr = cast[ptr UncheckedArray[ReprCString]](v.ptr)
for i in 0 ..< int(v.len):
result[i] = $arr[i]
## Convert a VecUint8 to a seq[byte]
proc toSeq*(v: VecUint8): seq[byte] =
if v.ptr == nil or v.len == 0:
return @[]
result = newSeq[byte](v.len)
copyMem(addr result[0], v.ptr, v.len)
## Access payloads from VecPayload
proc `[]`*(v: VecPayload, i: int): Payload =
assert i >= 0 and csize_t(i) < v.len
cast[ptr UncheckedArray[Payload]](v.ptr)[i]
## Get length of VecPayload
proc len*(v: VecPayload): int =
int(v.len)
## Iterator for VecPayload
iterator items*(v: VecPayload): Payload =
for i in 0 ..< v.len:
yield v[int(i)]
## Convert a string to seq[byte]
proc toBytes*(s: string): seq[byte] =
if s.len == 0:
return @[]
result = newSeq[byte](s.len)
copyMem(addr result[0], unsafeAddr s[0], s.len)