diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7e0481..9303db1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 2df56b0..5d31f18 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 1aeb5b8..7312bf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 2b6ddda..c841526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/core/conversations/Cargo.toml b/core/conversations/Cargo.toml index 7e681ac..9336240 100644 --- a/core/conversations/Cargo.toml +++ b/core/conversations/Cargo.toml @@ -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"] } diff --git a/core/conversations/src/types.rs b/core/conversations/src/types.rs index 892df43..1433425 100644 --- a/core/conversations/src/types.rs +++ b/core/conversations/src/types.rs @@ -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. diff --git a/core/double-ratchets/Cargo.toml b/core/double-ratchets/Cargo.toml index ded93b4..3e3091d 100644 --- a/core/double-ratchets/Cargo.toml +++ b/core/double-ratchets/Cargo.toml @@ -4,7 +4,7 @@ version = "0.0.1" edition = "2024" [lib] -crate-type = ["rlib", "cdylib"] +crate-type = ["rlib"] [dependencies] # Workspace dependencies (sorted) diff --git a/crates/client-ffi/Cargo.toml b/crates/client-ffi/Cargo.toml deleted file mode 100644 index 8b63c73..0000000 --- a/crates/client-ffi/Cargo.toml +++ /dev/null @@ -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"] diff --git a/crates/client-ffi/examples/message-exchange/Makefile b/crates/client-ffi/examples/message-exchange/Makefile deleted file mode 100644 index 0c92713..0000000 --- a/crates/client-ffi/examples/message-exchange/Makefile +++ /dev/null @@ -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) diff --git a/crates/client-ffi/examples/message-exchange/README.md b/crates/client-ffi/examples/message-exchange/README.md deleted file mode 100644 index 9160f70..0000000 --- a/crates/client-ffi/examples/message-exchange/README.md +++ /dev/null @@ -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 -``` diff --git a/crates/client-ffi/examples/message-exchange/src/main.c b/crates/client-ffi/examples/message-exchange/src/main.c deleted file mode 100644 index db07939..0000000 --- a/crates/client-ffi/examples/message-exchange/src/main.c +++ /dev/null @@ -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 -#include -#include -#include -#include -#include - -/* ------------------------------------------------------------------ - * 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; -} diff --git a/crates/client-ffi/src/api.rs b/crates/client-ffi/src/api.rs deleted file mode 100644 index a1d1400..0000000 --- a/crates/client-ffi/src/api.rs +++ /dev/null @@ -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, - events: Receiver, - inbound: Sender>, -} - -// --------------------------------------------------------------------------- -// 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 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>, -} - -#[derive_ReprC] -#[repr(opaque)] -pub struct CreateConvoResult { - error_code: i32, - convo_id: Option, -} - -/// 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, -} - -enum EventRow { - ConversationStarted { - convo_id: String, - class: FfiConversationClass, - }, - MessageReceived { - convo_id: String, - content: Vec, - }, -} - -impl EventRow { - /// Translate an [`Event`] into the FFI row shape, or `None` for variants - /// without an FFI representation. - fn from_event(event: Event) -> Option { - 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> { - 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) { - 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 { - handle - .client - .installation_name() - .as_bytes() - .to_vec() - .into_boxed_slice() - .into() -} - -#[ffi_export] -fn client_installation_name_free(name: c_slice::Box) { - 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 { - 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) { - 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 { - 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) { - 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 { - 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 { - 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) { - drop(list) -} diff --git a/crates/client-ffi/src/bin/generate-headers.rs b/crates/client-ffi/src/bin/generate-headers.rs deleted file mode 100644 index e985141..0000000 --- a/crates/client-ffi/src/bin/generate-headers.rs +++ /dev/null @@ -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) -} diff --git a/crates/client-ffi/src/delivery.rs b/crates/client-ffi/src/delivery.rs deleted file mode 100644 index 76fea0f..0000000 --- a/crates/client-ffi/src/delivery.rs +++ /dev/null @@ -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>>, -} - -impl CDelivery { - pub fn new(callback: DeliverFn, inbound: Receiver>) -> 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> { - self.inbound_rx - .take() - .expect("CDelivery::inbound called more than once") - } -} diff --git a/crates/client-ffi/src/lib.rs b/crates/client-ffi/src/lib.rs deleted file mode 100644 index ba391a3..0000000 --- a/crates/client-ffi/src/lib.rs +++ /dev/null @@ -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() -} diff --git a/docs/adr/0001-client-event-system.md b/docs/adr/0001-client-event-system.md index 14c819b..35add0c 100644 --- a/docs/adr/0001-client-event-system.md +++ b/docs/adr/0001-client-event-system.md @@ -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 diff --git a/flake.nix b/flake.nix index b31738a..6b7a746 100644 --- a/flake.nix +++ b/flake.nix @@ -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; - }; - }; } ); diff --git a/nim-bindings/src/bindings.nim b/nim-bindings/src/bindings.nim deleted file mode 100644 index b1c61da..0000000 --- a/nim-bindings/src/bindings.nim +++ /dev/null @@ -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)