mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-08 08:03:20 +00:00
Merge branch 'main' of github.com:logos-messaging/libchat into jazzz/big_merge
This commit is contained in:
commit
848cb193d1
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
@ -54,7 +54,6 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- run: rustup update stable && rustup default stable
|
|
||||||
- uses: cachix/install-nix-action@v31
|
- uses: cachix/install-nix-action@v31
|
||||||
with:
|
with:
|
||||||
nix_version: 2.34.6
|
nix_version: 2.34.6
|
||||||
@ -64,19 +63,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
primary-key: nix-${{ runner.os }}-fixtest-${{ hashFiles('flake.nix', 'flake.lock') }}
|
primary-key: nix-${{ runner.os }}-fixtest-${{ hashFiles('flake.nix', 'flake.lock') }}
|
||||||
restore-prefixes-first-match: nix-${{ runner.os }}-
|
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
|
- name: Build logos-delivery
|
||||||
# Build through a patched nixpkgs (kaichaosun/nixpkgs fix-gitfetch),
|
# Build through a patched nixpkgs (kaichaosun/nixpkgs fix-gitfetch),
|
||||||
# whose nix-prefetch-git disables git background auto-maintenance so the
|
# whose nix-prefetch-git disables git background auto-maintenance so the
|
||||||
@ -96,24 +82,3 @@ jobs:
|
|||||||
run: nix develop -c bash -c 'LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli'
|
run: nix develop -c bash -c 'LOGOS_DELIVERY_LIB_DIR=./result/lib cargo build --release -p chat-cli'
|
||||||
- name: Run chat-cli smoketest
|
- name: Run chat-cli smoketest
|
||||||
run: nix develop -c ./target/release/chat-cli --name ci-test --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
11
.gitignore
vendored
@ -22,11 +22,6 @@ target
|
|||||||
|
|
||||||
*/.DS_Store
|
*/.DS_Store
|
||||||
|
|
||||||
# Compiled binary
|
|
||||||
**/ffi_nim_example
|
|
||||||
/nim-bindings/examples/pingpong
|
|
||||||
/nim-bindings/libchat
|
|
||||||
|
|
||||||
# Temporary data folder
|
# Temporary data folder
|
||||||
tmp
|
tmp
|
||||||
|
|
||||||
@ -34,9 +29,3 @@ tmp
|
|||||||
result
|
result
|
||||||
|
|
||||||
.DS_Store
|
.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
|
|
||||||
|
|||||||
195
Cargo.lock
generated
195
Cargo.lock
generated
@ -1409,16 +1409,6 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "client-ffi"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-channel",
|
|
||||||
"libchat",
|
|
||||||
"logos-chat",
|
|
||||||
"safer-ffi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@ -2123,41 +2113,6 @@ version = "3.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
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]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@ -3097,15 +3052,6 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inventory"
|
|
||||||
version = "0.3.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
|
||||||
dependencies = [
|
|
||||||
"rustversion",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@ -3311,7 +3257,6 @@ dependencies = [
|
|||||||
"prost",
|
"prost",
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"safer-ffi",
|
|
||||||
"shared-traits",
|
"shared-traits",
|
||||||
"storage",
|
"storage",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
@ -3699,22 +3644,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -4394,16 +4323,6 @@ dependencies = [
|
|||||||
"zerocopy",
|
"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]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@ -4514,7 +4433,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"prettyplease 0.2.37",
|
"prettyplease",
|
||||||
"prost",
|
"prost",
|
||||||
"prost-types",
|
"prost-types",
|
||||||
"regex",
|
"regex",
|
||||||
@ -5212,38 +5131,6 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
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]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@ -5524,12 +5411,6 @@ dependencies = [
|
|||||||
"digest 0.11.3",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2-const-stable"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha3"
|
name = "sha3"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -5670,41 +5551,6 @@ dependencies = [
|
|||||||
"der",
|
"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 0.4.1",
|
|
||||||
"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]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -6257,15 +6103,6 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
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]]
|
[[package]]
|
||||||
name = "universal-hash"
|
name = "universal-hash"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -6282,12 +6119,6 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unwind_safe"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@ -6840,7 +6671,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"prettyplease 0.2.37",
|
"prettyplease",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
"wasm-metadata",
|
"wasm-metadata",
|
||||||
"wit-bindgen-core",
|
"wit-bindgen-core",
|
||||||
@ -6854,7 +6685,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"prettyplease 0.2.37",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
@ -6899,26 +6730,6 @@ dependencies = [
|
|||||||
"wasmparser",
|
"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]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
|||||||
@ -12,7 +12,6 @@ members = [
|
|||||||
"core/shared-traits",
|
"core/shared-traits",
|
||||||
"core/sqlite",
|
"core/sqlite",
|
||||||
"core/storage",
|
"core/storage",
|
||||||
"crates/client-ffi",
|
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"extensions/components",
|
"extensions/components",
|
||||||
]
|
]
|
||||||
@ -26,7 +25,6 @@ default-members = [
|
|||||||
"core/shared-traits",
|
"core/shared-traits",
|
||||||
"core/sqlite",
|
"core/sqlite",
|
||||||
"core/storage",
|
"core/storage",
|
||||||
"crates/client-ffi",
|
|
||||||
"crates/client",
|
"crates/client",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -45,7 +43,7 @@ storage = { path = "core/storage" }
|
|||||||
blake2 = "0.10"
|
blake2 = "0.10"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
|
|
||||||
# Panicking across FFI boundaries is UB; abort is the correct strategy for a
|
# Panicking across FFI boundaries is UB; chat-cli registers Rust callbacks
|
||||||
# C FFI library.
|
# that liblogosdelivery invokes, so abort instead of unwinding.
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|||||||
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["rlib","staticlib"]
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
@ -29,7 +29,6 @@ openmls_traits = "0.5.0"
|
|||||||
prost = "0.14.1"
|
prost = "0.14.1"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
rand_core = { version = "0.6" }
|
rand_core = { version = "0.6" }
|
||||||
safer-ffi = "0.1.13"
|
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }
|
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "reusable_secrets", "getrandom"] }
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::fmt::Debug;
|
|||||||
|
|
||||||
use crate::proto::{self, Message};
|
use crate::proto::{self, Message};
|
||||||
|
|
||||||
// FFI Type definitions
|
// Public type definitions
|
||||||
|
|
||||||
// This struct represents Outbound data.
|
// This struct represents Outbound data.
|
||||||
// It wraps an encoded payload with a delivery address, so it can be handled by the delivery service.
|
// It wraps an encoded payload with a delivery address, so it can be handled by the delivery service.
|
||||||
|
|||||||
@ -4,7 +4,7 @@ version = "0.0.1"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["rlib", "cdylib"]
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Workspace dependencies (sorted)
|
# Workspace dependencies (sorted)
|
||||||
|
|||||||
@ -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"]
|
|
||||||
@ -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)
|
|
||||||
@ -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
|
|
||||||
```
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
@ -5,7 +5,7 @@
|
|||||||
| Status | Accepted |
|
| Status | Accepted |
|
||||||
| Issue | https://github.com/logos-messaging/libchat/issues/97 |
|
| Issue | https://github.com/logos-messaging/libchat/issues/97 |
|
||||||
| Date | 2026-05-19 |
|
| Date | 2026-05-19 |
|
||||||
| Last revised | 2026-06-09 |
|
| Last revised | 2026-06-11 |
|
||||||
|
|
||||||
## Context and Problem
|
## 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.
|
- **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.
|
- **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
|
## Architecture
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ flowchart TB
|
|||||||
B == "Event (async channel)" ==> A
|
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
|
## Decisions
|
||||||
|
|
||||||
|
|||||||
54
flake.nix
54
flake.nix
@ -27,61 +27,13 @@
|
|||||||
});
|
});
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = forAllSystems ({ pkgs, system }:
|
packages = forAllSystems ({ system, ... }:
|
||||||
let
|
{
|
||||||
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust_toolchain.toml;
|
logos-delivery = logos-delivery.packages.${system}.liblogosdelivery.override {
|
||||||
rustPlatform = pkgs.makeRustPlatform {
|
|
||||||
cargo = rustToolchain;
|
|
||||||
rustc = rustToolchain;
|
|
||||||
};
|
|
||||||
logos-delivery-lib = logos-delivery.packages.${system}.liblogosdelivery.override {
|
|
||||||
enablePostgres = false;
|
enablePostgres = false;
|
||||||
enableNimDebugDlOpen = false;
|
enableNimDebugDlOpen = false;
|
||||||
chroniclesLogLevel = "FATAL";
|
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=";
|
|
||||||
"de-mls-3.0.0" = "sha256-ZUbQDHUOqLVsETK8KXbjmVwh/mGOMFl3bisIJfndc74=";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.perl pkgs.pkg-config pkgs.cmake pkgs.protobuf ];
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
|
||||||
Loading…
x
Reference in New Issue
Block a user