├── core ├── LICENSE ├── README.md ├── src │ ├── util.rs │ ├── util │ │ ├── send_marker.rs │ │ ├── raw_const_ptr.rs │ │ └── owned_slice.rs │ ├── account │ │ ├── balance.rs │ │ └── filter.rs │ ├── callback.rs │ ├── packet.rs │ ├── account.rs │ ├── query_filter.rs │ ├── transfer.rs │ ├── lib.rs │ └── error.rs ├── Cargo.toml └── examples │ └── c_port_low_level.rs ├── sys ├── LICENSE ├── README.md ├── src │ ├── wrapper.h │ ├── tb_client.h │ └── lib.rs ├── Cargo.toml └── build.rs ├── .gitignore ├── .gitmodules ├── .editorconfig ├── .github ├── dependabot.yml ├── workflows │ ├── check-new-ver.yml │ └── ci.yml └── upgrade-ver.sh ├── CONTRIBUTING.md ├── .dockerignore ├── Cargo.toml ├── README.md ├── src ├── reply.rs ├── id.rs └── lib.rs ├── examples └── c_port_high_level.rs ├── LICENSE └── CHANGELOG.md /core/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /sys/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /sys/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /sys/src/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "tb_client.h" 2 | -------------------------------------------------------------------------------- /sys/src/tb_client.h: -------------------------------------------------------------------------------- 1 | ../tigerbeetle/src/clients/c/tb_client.h -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | 4 | # Backup files produced by CLI tools. 5 | *.bk 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sys/tigerbeetle"] 2 | path = sys/tigerbeetle 3 | url = https://github.com/tigerbeetledb/tigerbeetle 4 | -------------------------------------------------------------------------------- /core/src/util.rs: -------------------------------------------------------------------------------- 1 | //! Helpful abstractions to generalize over various types 2 | 3 | mod owned_slice; 4 | mod raw_const_ptr; 5 | pub mod send_marker; 6 | 7 | pub use owned_slice::*; 8 | pub use raw_const_ptr::RawConstPtr; 9 | pub use send_marker::SendMarker; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.md] 10 | indent_style = space 11 | indent_size = 4 12 | max_line_length = off 13 | 14 | [*.{yaml,yml}] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.sh] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: / 5 | labels: ["enhancement", "rust", "k::dependencies"] 6 | schedule: 7 | interval: daily 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | labels: ["enhancement", "github_actions", "k::dependencies", "k::toolchain"] 12 | schedule: 13 | interval: daily 14 | -------------------------------------------------------------------------------- /core/src/util/send_marker.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | pub struct Unsendable(PhantomData<*const ()>); 4 | 5 | unsafe impl Sync for Unsendable {} 6 | 7 | pub struct Sendable(()); 8 | 9 | pub trait SendMarker: send_marker_seal::Sealed {} 10 | 11 | impl SendMarker for Sendable {} 12 | 13 | impl SendMarker for Unsendable {} 14 | 15 | pub(crate) mod send_marker_seal { 16 | pub trait Sealed {} 17 | impl Sealed for super::Sendable {} 18 | impl Sealed for super::Unsendable {} 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribution Guide 2 | ================== 3 | 4 | 5 | 6 | 7 | ## Upgrading to new [TigerBeetle] version 8 | 9 | First, checkout Git submodule to the desired version: 10 | ```bash 11 | cd sys/tigerbeetle 12 | git fetch origin 'refs/tags/*:refs/tags/*' 13 | git checkout 0.15.3 14 | ``` 15 | 16 | Then, update `TIGERBEETLE_RELEASE` and `TIGERBEETLE_VERSION` in the [build script](./sys/build.rs). 17 | ```rust 18 | pub const TIGERBEETLE_RELEASE: &str = "0.15.3"; 19 | pub const TIGERBEETLE_COMMIT: &str = "73bbc1a32ba2513e369764680350c099fe302285"; 20 | ``` 21 | 22 | Finally, provide changes supporting new [TigerBeetle] version. 23 | 24 | 25 | 26 | 27 | [TigerBeetle]: https://tigerbeetle.com 28 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.DS_Store 8 | **/.classpath 9 | **/.dockerignore 10 | **/.env 11 | **/.git 12 | **/.gitignore 13 | **/.project 14 | **/.settings 15 | **/.toolstarget 16 | **/.vs 17 | **/.vscode 18 | **/*.*proj.user 19 | **/*.dbmdl 20 | **/*.jfm 21 | **/charts 22 | **/docker-compose* 23 | **/compose* 24 | **/Dockerfile* 25 | **/node_modules 26 | **/npm-debug.log 27 | **/secrets.dev.yaml 28 | **/values.dev.yaml 29 | /bin 30 | /target 31 | LICENSE 32 | README.md 33 | -------------------------------------------------------------------------------- /sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tigerbeetle-unofficial-sys" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition = "2021" 6 | rust-version.workspace = true 7 | description = "Native bindings to the tigerbeetle library" 8 | repository.workspace = true 9 | license.workspace = true 10 | categories = ["external-ffi-bindings"] 11 | 12 | [features] 13 | generated-safe = [] 14 | 15 | [dependencies] 16 | bitflags = "2.6" 17 | bytemuck = { version = "1.19", features = ["derive", "min_const_generics"] } 18 | 19 | [build-dependencies] 20 | bindgen = "0.72" 21 | proc-macro2 = { version = "1.0.80", default-features = false } 22 | quote = { version = "1.0", default-features = false } 23 | syn = { version = "2.0", features = ["parsing", "full", "printing", "visit", "visit-mut"], default-features = false } 24 | -------------------------------------------------------------------------------- /core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tigerbeetle-unofficial-core" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition = "2021" 6 | rust-version.workspace = true 7 | description = "Safe low level callback-based async bindings to tigerbeetle client library." 8 | repository.workspace = true 9 | license.workspace = true 10 | categories = ["api-bindings", "asynchronous", "database", "finance"] 11 | include = ["/src/**", "/examples/**", "/Cargo.toml", "/LICENSE-*", "/README"] 12 | 13 | [features] 14 | tokio = ["dep:tokio"] 15 | tokio-rt-multi-thread = ["dep:tokio", "tokio/rt-multi-thread"] 16 | 17 | [dependencies] 18 | bytemuck = "1.19" 19 | sptr = "0.3.2" 20 | sys = { version = "=0.14.17+0.16.67", package = "tigerbeetle-unofficial-sys", path = "../sys", features = ["generated-safe"] } 21 | tokio = { version = "1.28.1", optional = true } 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tigerbeetle-unofficial" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition = "2021" 6 | rust-version.workspace = true 7 | description = "Safe high level async bindings to tigerbeetle client library." 8 | repository.workspace = true 9 | license.workspace = true 10 | categories = ["api-bindings", "asynchronous", "database", "finance"] 11 | include = ["/src/**", "/examples/**", "/Cargo.toml", "/LICENSE-*", "README.md", "CHANGELOG.md"] 12 | 13 | [features] 14 | tokio-rt-multi-thread = ["core/tokio-rt-multi-thread"] 15 | 16 | [dependencies] 17 | bytemuck = { version = "1.16", features = ["extern_crate_alloc"] } 18 | core = { version = "=0.14.17+0.16.67", package = "tigerbeetle-unofficial-core", path = "core" } 19 | fastrand = "2.3" 20 | tokio = { version = "1.28.1", features = ["sync"] } 21 | 22 | [dev-dependencies] 23 | pollster = { version = "0.4", features = ["macro"] } 24 | 25 | [workspace] 26 | members = ["sys", "core"] 27 | 28 | [workspace.package] 29 | version = "0.14.17+0.16.67" 30 | authors = ["Daria Sukhonina "] 31 | rust-version = "1.78" 32 | repository = "https://github.com/tigerbeetle-rust/tigerbeetle-unofficial" 33 | license = "Apache-2.0" 34 | -------------------------------------------------------------------------------- /.github/workflows/check-new-ver.yml: -------------------------------------------------------------------------------- 1 | name: Check new version 2 | 3 | on: 4 | schedule: 5 | - cron: "42 7 * * *" 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - run: .github/upgrade-ver.sh 14 | id: new 15 | 16 | - name: Check whether Pull Request branch exists already 17 | id: pr-branch 18 | run: echo "exists=$((git ls-remote --heads origin ${branch} | grep ${branch} > /dev/null) 19 | && echo 'true' || echo 'false')" 20 | >> $GITHUB_OUTPUT 21 | env: 22 | branch: upgrade-to-tigerbeetle-${{ steps.new.outputs.version }} 23 | 24 | - name: Create Pull Request 25 | uses: peter-evans/create-pull-request@v8 26 | with: 27 | commit-message: Upgrade to ${{ steps.new.outputs.version }} TigerBeetle 28 | branch: upgrade-to-tigerbeetle-${{ steps.new.outputs.version }} 29 | delete-branch: true 30 | draft: true 31 | sign-commits: true 32 | signoff: true 33 | title: Upgrade to ${{ steps.new.outputs.version }} TigerBeetle 34 | body: Upgrade to new ${{ steps.new.outputs.version }} version of TigerBeetle. 35 | labels: | 36 | enhancement 37 | k::dependencies 38 | k::version 39 | zig 40 | if: ${{ steps.pr-branch.outputs.exists == 'false' }} 41 | -------------------------------------------------------------------------------- /.github/upgrade-ver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | latestReleaseInfo=$(curl -s https://api.github.com/repos/tigerbeetle/tigerbeetle/releases/latest | jq .) 6 | 7 | newVersion=$(echo "$latestReleaseInfo" | jq -r '.tag_name') 8 | if [[ "$newVersion" = "null" ]]; then 9 | echo -e "Wrong JSON response: no \`tag_name\` field found." 10 | exit 1 11 | fi 12 | newCommit=$(echo "$latestReleaseInfo" | jq -r '.target_commitish') 13 | if [[ "$newVersion" = "null" ]]; then 14 | echo -e "Wrong JSON response: no \`target_commitish\` field found." 15 | exit 1 16 | fi 17 | 18 | echo "version=$newVersion" 19 | echo "commit=$newCommit" 20 | if [[ ! -z "$GITHUB_OUTPUT" ]]; then 21 | echo "version=$newVersion" >> $GITHUB_OUTPUT 22 | echo "commit=$newCommit" >> $GITHUB_OUTPUT 23 | fi 24 | 25 | if [[ -z $(echo "$latestReleaseInfo" | jq -r 'select(.draft == false)') ]]; then 26 | echo "Latest TigerBeetle $newVersion release is in draft yet. Skipping..." 27 | exit 0 28 | fi 29 | if [[ -z $(echo "$latestReleaseInfo" | jq -r 'select(.prerelease == false)') ]]; then 30 | echo "Latest TigerBeetle $newVersion version is pre-release. Skipping..." 31 | exit 0 32 | fi 33 | 34 | sed -i.bk -e "s/^const TIGERBEETLE_RELEASE: \&str =.*$/const TIGERBEETLE_RELEASE: \&str = \"$newVersion\";/g" \ 35 | -e "s/^const TIGERBEETLE_COMMIT: \&str =.*$/const TIGERBEETLE_COMMIT: \&str = \"$newCommit\";/g" \ 36 | ./sys/build.rs 37 | if cmp -s ./sys/build.rs ./sys/build.rs.bk; then 38 | echo "TigerBeetle $newVersion version is already the latest one. Skipping... " 39 | fi 40 | -------------------------------------------------------------------------------- /core/src/account/balance.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use bytemuck::{Pod, TransparentWrapper, Zeroable}; 4 | 5 | pub use sys::tb_account_balance_t as Raw; 6 | 7 | #[repr(transparent)] 8 | #[derive(Clone, Copy, TransparentWrapper, Pod, Zeroable)] 9 | pub struct Balance(Raw); 10 | 11 | impl Balance { 12 | pub const fn from_raw(raw: Raw) -> Self { 13 | Balance(raw) 14 | } 15 | pub const fn into_raw(self) -> Raw { 16 | self.0 17 | } 18 | pub const fn as_raw(&self) -> &Raw { 19 | &self.0 20 | } 21 | pub fn as_raw_mut(&mut self) -> &mut Raw { 22 | &mut self.0 23 | } 24 | 25 | pub const fn debits_pending(&self) -> u128 { 26 | self.0.debits_pending 27 | } 28 | pub fn set_debits_pending(&mut self, debits_pending: u128) { 29 | self.0.debits_pending = debits_pending; 30 | } 31 | pub const fn with_debits_pending(mut self, debits_pending: u128) -> Self { 32 | self.0.debits_pending = debits_pending; 33 | self 34 | } 35 | 36 | pub const fn debits_posted(&self) -> u128 { 37 | self.0.debits_posted 38 | } 39 | pub fn set_debits_posted(&mut self, debits_posted: u128) { 40 | self.0.debits_posted = debits_posted; 41 | } 42 | pub const fn with_debits_posted(mut self, debits_posted: u128) -> Self { 43 | self.0.debits_posted = debits_posted; 44 | self 45 | } 46 | 47 | pub const fn credits_pending(&self) -> u128 { 48 | self.0.credits_pending 49 | } 50 | pub fn set_credits_pending(&mut self, credits_pending: u128) { 51 | self.0.credits_pending = credits_pending; 52 | } 53 | pub const fn with_credits_pending(mut self, credits_pending: u128) -> Self { 54 | self.0.credits_pending = credits_pending; 55 | self 56 | } 57 | 58 | pub const fn credits_posted(&self) -> u128 { 59 | self.0.credits_posted 60 | } 61 | pub fn set_credits_posted(&mut self, credits_posted: u128) { 62 | self.0.credits_posted = credits_posted; 63 | } 64 | pub const fn with_credits_posted(mut self, credits_posted: u128) -> Self { 65 | self.0.credits_posted = credits_posted; 66 | self 67 | } 68 | 69 | pub fn timestamp(&self) -> SystemTime { 70 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp) 71 | } 72 | } 73 | 74 | impl std::fmt::Debug for Balance { 75 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 76 | f.debug_struct("AccountBalance") 77 | .field("debits_pending", &self.0.debits_pending) 78 | .field("debits_posted", &self.0.debits_posted) 79 | .field("credits_pending", &self.0.credits_pending) 80 | .field("credits_pending", &self.0.credits_pending) 81 | .field("timestamp", &self.0.timestamp) 82 | .finish_non_exhaustive() 83 | } 84 | } 85 | 86 | impl From for Balance { 87 | fn from(value: Raw) -> Self { 88 | Balance(value) 89 | } 90 | } 91 | impl From for Raw { 92 | fn from(value: Balance) -> Self { 93 | value.0 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc( 2 | html_logo_url = "https://avatars.githubusercontent.com/u/187310527", 3 | html_favicon_url = "https://avatars.githubusercontent.com/u/187310527?s=256" 4 | )] 5 | #![allow(non_upper_case_globals)] 6 | #![allow(non_camel_case_types)] 7 | #![allow(non_snake_case)] 8 | 9 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 10 | 11 | /// Available only with `generated-safe` feature 12 | #[cfg(feature = "generated-safe")] 13 | #[allow(clippy::unnecessary_cast, clippy::assign_op_pattern)] 14 | #[doc(hidden)] 15 | pub mod generated_safe { 16 | include!(concat!(env!("OUT_DIR"), "/generated.rs")); 17 | } 18 | 19 | #[cfg(test)] 20 | mod linked { 21 | //! Bunch of dummies to ensure eagerly that everything is linked properly. 22 | //! 23 | //! Executing them will definitely cause undefined behaviour, so they're ignored, which is 24 | //! enough to check the linker does its job. 25 | 26 | use std::{mem, ptr}; 27 | 28 | use bytemuck::Zeroable as _; 29 | 30 | #[test] 31 | #[ignore = "only checks linkage"] 32 | fn tb_client_init() { 33 | unsafe { 34 | let mut raw = mem::zeroed(); 35 | let address = "3000".as_bytes(); 36 | _ = crate::tb_client_init( 37 | &mut raw, 38 | 1_u128.to_le_bytes().as_ptr(), 39 | address.as_ptr().cast(), 40 | address.len().try_into().unwrap(), 41 | ptr::null::<()>() as usize, 42 | None, 43 | ); 44 | } 45 | } 46 | 47 | #[test] 48 | #[ignore = "only checks linkage"] 49 | fn tb_client_init_echo() { 50 | unsafe { 51 | let mut raw = mem::zeroed(); 52 | let address = "3000".as_bytes(); 53 | _ = crate::tb_client_init_echo( 54 | &mut raw, 55 | 1_u128.to_le_bytes().as_ptr(), 56 | address.as_ptr().cast(), 57 | address.len().try_into().unwrap(), 58 | ptr::null::<()>() as usize, 59 | None, 60 | ); 61 | } 62 | } 63 | 64 | #[test] 65 | #[ignore = "only checks linkage"] 66 | fn tb_client_init_parameters() { 67 | unsafe { 68 | let mut raw = mem::zeroed(); 69 | _ = crate::tb_client_init_parameters( 70 | &mut raw, 71 | &mut crate::tb_init_parameters_t::zeroed(), 72 | ); 73 | } 74 | } 75 | 76 | #[test] 77 | #[ignore = "only checks linkage"] 78 | fn tb_client_completion_context() { 79 | unsafe { 80 | let mut client = mem::zeroed(); 81 | _ = crate::tb_client_completion_context(&mut client, ptr::null_mut()); 82 | } 83 | } 84 | 85 | #[test] 86 | #[ignore = "only checks linkage"] 87 | fn tb_client_submit() { 88 | unsafe { 89 | let mut client = mem::zeroed(); 90 | _ = crate::tb_client_submit(&mut client, ptr::null_mut()); 91 | } 92 | } 93 | 94 | #[test] 95 | #[ignore = "only checks linkage"] 96 | fn tb_client_deinit() { 97 | unsafe { 98 | let mut client = mem::zeroed(); 99 | _ = crate::tb_client_deinit(&mut client); 100 | } 101 | } 102 | 103 | #[test] 104 | #[ignore = "only checks linkage"] 105 | fn tb_client_register_log_callback() { 106 | unsafe { 107 | _ = crate::tb_client_register_log_callback(None, false); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `tigerbeetle-unofficial` 2 | ======================== 3 | 4 | [![crates.io](https://img.shields.io/crates/v/tigerbeetle-unofficial.svg "crates.io")](https://crates.io/crates/tigerbeetle-unofficial) 5 | [![Rust 1.78+](https://img.shields.io/badge/rustc-1.78+-lightgray.svg "Rust 1.78+")](https://blog.rust-lang.org/2024/05/02/Rust-1.78.0.html)\ 6 | [![CI](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/actions/workflows/ci.yml/badge.svg?branch=main "CI")](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/actions?query=workflow%3ACI+branch%3Amain) 7 | [![Rust docs](https://docs.rs/tigerbeetle-unofficial/badge.svg "Rust docs")](https://docs.rs/tigerbeetle-unofficial) 8 | 9 | [Changelog](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/blob/v0.14.17%2B0.16.67/CHANGELOG.md) 10 | 11 | Unofficial [TigerBeetle] bindings for [Rust]. 12 | 13 | > **WARNING**: In [TigerBeetle] a client is **not [backward compatible][2]** with a cluster. You cannot run a newer client against an older cluster: clients are only [forward compatible][3] with replicas from their own version or newer (see the ["Oldest supported client version"](https://github.com/tigerbeetle/tigerbeetle/releases/tag/0.16.67) for the supported versions range). 14 | > To avoid accidental use of a newer [TigerBeetle] client with an older cluster, it's highly recommended to pin the exact version if this crate in your `[dependencies]` and only change it with the cluster upgrade along: 15 | > ```toml 16 | > [dependencies] 17 | > tigerbeetle-unofficial = "=0.14.17+0.16.67" 18 | > ``` 19 | 20 | 21 | 22 | 23 | ## Status 24 | 25 | Because this [TigerBeetle] client library implementation is not a part of the [official `tigerbeetle` repos][1], it is hard to ensure and keep some of [Rust] safety guarantees from the outside. For that reason I invite people to contribute to this repo or finally develop the official [Rust] client library. 26 | 27 | 28 | 29 | 30 | ## Repo Overview 31 | 32 | The repository hosts the following libraries: 33 | 34 | * [![Crates.io](https://img.shields.io/crates/v/tigerbeetle-unofficial.svg?label=tigerbeetle-unofficial)](https://crates.io/crates/tigerbeetle-unofficial) 35 | [![docs.rs](https://docs.rs/tigerbeetle-unofficial/badge.svg)](https://docs.rs/tigerbeetle-unofficial) - Safe high-level async bindings. Implemented with `#![forbid(unsafe_code)]` upon `tigerbeetle-unofficial-core`. 36 | * [![Crates.io](https://img.shields.io/crates/v/tigerbeetle-unofficial-core.svg?label=tigerbeetle-unofficial-core)](https://crates.io/crates/tigerbeetle-unofficial-core) 37 | [![docs.rs](https://docs.rs/tigerbeetle-unofficial-core/badge.svg)](https://docs.rs/tigerbeetle-unofficial-core) - Safe low-level callback-based async bindings. 38 | * [![Crates.io](https://img.shields.io/crates/v/tigerbeetle-unofficial-sys.svg?label=tigerbeetle-unofficial-sys)](https://crates.io/crates/tigerbeetle-unofficial-sys) 39 | [![docs.rs](https://docs.rs/tigerbeetle-unofficial-sys/badge.svg)](https://docs.rs/tigerbeetle-unofficial-sys) - Unsafe native bindings. 40 | 41 | 42 | 43 | 44 | 45 | ## License 46 | 47 | [TigerBeetle] is licensed under [Apache License, Version 2.0](https://github.com/tigerbeetle/tigerbeetle/blob/0.16.67/LICENSE). 48 | 49 | `tigerbeetle-unofficial` crates are licensed under the [Apache License, Version 2.0](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/blob/v0.14.17%2B0.16.67/LICENSE) (the "License"); you may not use these files except in compliance with the License. You may obtain a copy of the License at 50 | 51 | https://www.apache.org/licenses/LICENSE-2.0 52 | 53 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 54 | 55 | 56 | 57 | 58 | [Rust]: https://www.rust-lang.org 59 | [TigerBeetle]: https://tigerbeetle.com 60 | [1]: https://github.com/tigerbeetle 61 | [2]: https://en.wikipedia.org/wiki/Backward_compatibility 62 | [3]: https://en.wikipedia.org/wiki/Forward_compatibility 63 | -------------------------------------------------------------------------------- /core/src/account/filter.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use bytemuck::{Pod, TransparentWrapper, Zeroable}; 4 | 5 | pub use sys::generated_safe::AccountFilterFlags as Flags; 6 | pub use sys::tb_account_filter_t as Raw; 7 | 8 | #[repr(transparent)] 9 | #[derive(Clone, Copy, TransparentWrapper, Pod, Zeroable)] 10 | pub struct Filter(Raw); 11 | 12 | impl std::fmt::Debug for Filter { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | f.debug_struct("AccountFilter") 15 | .field("account_id", &self.0.account_id) 16 | .field("timestamp_min", &self.timestamp_min()) 17 | .field("timestamp_max", &self.timestamp_max()) 18 | .field("limit", &self.0.limit) 19 | .field("flags", &self.flags()) 20 | .finish_non_exhaustive() 21 | } 22 | } 23 | 24 | impl Filter { 25 | #[track_caller] 26 | pub fn new(account_id: u128, limit: u32) -> Self { 27 | Filter(Raw::zeroed()) 28 | .with_account_id(account_id) 29 | .with_limit(limit) 30 | } 31 | 32 | pub const fn from_raw(raw: Raw) -> Self { 33 | Filter(raw) 34 | } 35 | pub const fn into_raw(self) -> Raw { 36 | self.0 37 | } 38 | pub const fn as_raw(&self) -> &Raw { 39 | &self.0 40 | } 41 | pub fn as_raw_mut(&mut self) -> &mut Raw { 42 | &mut self.0 43 | } 44 | 45 | pub const fn account_id(&self) -> u128 { 46 | self.0.account_id 47 | } 48 | pub fn set_account_id(&mut self, account_id: u128) { 49 | assert_ne!(account_id, u128::MAX, "account_id must not be `2^128 - 1`"); 50 | self.0.account_id = account_id; 51 | } 52 | pub const fn with_account_id(mut self, account_id: u128) -> Self { 53 | self.0.account_id = account_id; 54 | self 55 | } 56 | 57 | pub fn timestamp_min(&self) -> SystemTime { 58 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp_min) 59 | } 60 | pub fn set_timestamp_min(&mut self, timestamp_min: SystemTime) { 61 | let t = timestamp_min 62 | .duration_since(SystemTime::UNIX_EPOCH) 63 | .ok() 64 | .and_then(|t| t.as_nanos().try_into().ok()) 65 | .expect("failed to get nanoseconds since unix epoch from the argument"); 66 | assert_ne!(t, u64::MAX, "timestamp_min must not be `2^64 - 1`"); 67 | self.0.timestamp_min = t; 68 | } 69 | pub fn with_timestamp_min(mut self, timestamp_min: SystemTime) -> Self { 70 | self.set_timestamp_min(timestamp_min); 71 | self 72 | } 73 | 74 | pub fn timestamp_max(&self) -> SystemTime { 75 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp_max) 76 | } 77 | pub fn set_timestamp_max(&mut self, timestamp_max: SystemTime) { 78 | let t = timestamp_max 79 | .duration_since(SystemTime::UNIX_EPOCH) 80 | .ok() 81 | .and_then(|t| t.as_nanos().try_into().ok()) 82 | .expect("failed to get nanoseconds since unix epoch from the argument"); 83 | assert_ne!(t, u64::MAX, "timestamp_max must not be `2^64 - 1`"); 84 | self.0.timestamp_max = t; 85 | } 86 | pub fn with_timestamp_max(mut self, timestamp_max: SystemTime) -> Self { 87 | self.set_timestamp_max(timestamp_max); 88 | self 89 | } 90 | 91 | pub const fn limit(&self) -> u32 { 92 | self.0.limit 93 | } 94 | pub fn set_limit(&mut self, limit: u32) { 95 | assert_ne!(limit, 0, "limit must not be zero"); 96 | self.0.limit = limit; 97 | } 98 | pub fn with_limit(mut self, limit: u32) -> Self { 99 | self.set_limit(limit); 100 | self 101 | } 102 | 103 | pub const fn flags(&self) -> Flags { 104 | Flags::from_bits_retain(self.0.flags) 105 | } 106 | pub fn set_flags(&mut self, flags: Flags) { 107 | self.0.flags = flags.bits(); 108 | } 109 | pub const fn with_flags(mut self, flags: Flags) -> Self { 110 | self.0.flags = flags.bits(); 111 | self 112 | } 113 | } 114 | 115 | impl From for Filter { 116 | fn from(value: Raw) -> Self { 117 | Filter(value) 118 | } 119 | } 120 | impl From for Raw { 121 | fn from(value: Filter) -> Self { 122 | value.0 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/reply.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | account, 3 | core::OperationKind, 4 | error::{CreateAccountsApiError, CreateTransfersApiError}, 5 | Account, Transfer, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub enum Reply { 10 | CreateAccounts(Result<(), CreateAccountsApiError>), 11 | CreateTransfers(Result<(), CreateTransfersApiError>), 12 | GetAccountBalances(Vec), 13 | GetAccountTransfers(Vec), 14 | LookupAccounts(Vec), 15 | LookupTransfers(Vec), 16 | QueryAccounts(Vec), 17 | QueryTransfers(Vec), 18 | } 19 | 20 | impl Reply { 21 | pub fn copy_from_reply(operation: OperationKind, payload: &[u8]) -> Self { 22 | match operation { 23 | OperationKind::CreateAccounts => { 24 | let results = bytemuck::pod_collect_to_vec(payload); 25 | let e = CreateAccountsApiError::from_raw_results(results); 26 | Reply::CreateAccounts(e.map_or(Ok(()), Err)) 27 | } 28 | OperationKind::CreateTransfers => { 29 | let results = bytemuck::pod_collect_to_vec(payload); 30 | let e = CreateTransfersApiError::from_raw_results(results); 31 | Reply::CreateTransfers(e.map_or(Ok(()), Err)) 32 | } 33 | OperationKind::GetAccountBalances => { 34 | Reply::GetAccountBalances(bytemuck::pod_collect_to_vec(payload)) 35 | } 36 | OperationKind::GetAccountTransfers => { 37 | Reply::GetAccountTransfers(bytemuck::pod_collect_to_vec(payload)) 38 | } 39 | OperationKind::LookupAccounts => { 40 | Reply::LookupAccounts(bytemuck::pod_collect_to_vec(payload)) 41 | } 42 | OperationKind::LookupTransfers => { 43 | Reply::LookupTransfers(bytemuck::pod_collect_to_vec(payload)) 44 | } 45 | OperationKind::QueryAccounts => { 46 | Reply::QueryAccounts(bytemuck::pod_collect_to_vec(payload)) 47 | } 48 | OperationKind::QueryTransfers => { 49 | Reply::QueryTransfers(bytemuck::pod_collect_to_vec(payload)) 50 | } 51 | _ => unimplemented!("unknown `OperationKind`: {}", operation as u8), 52 | } 53 | } 54 | 55 | pub fn into_create_accounts(self) -> Result<(), CreateAccountsApiError> { 56 | if let Reply::CreateAccounts(out) = self { 57 | out 58 | } else { 59 | panic!("wrong reply variant, expected CreateAccounts but found: {self:?}") 60 | } 61 | } 62 | 63 | pub fn into_create_transfers(self) -> Result<(), CreateTransfersApiError> { 64 | if let Reply::CreateTransfers(out) = self { 65 | out 66 | } else { 67 | panic!("wrong reply variant, expected CreateTransfers but found: {self:?}") 68 | } 69 | } 70 | 71 | pub fn into_get_account_balances(self) -> Vec { 72 | if let Reply::GetAccountBalances(out) = self { 73 | out 74 | } else { 75 | panic!("wrong reply variant, expected GetAccountBalances but found: {self:?}") 76 | } 77 | } 78 | 79 | pub fn into_get_account_transfers(self) -> Vec { 80 | if let Reply::GetAccountTransfers(out) = self { 81 | out 82 | } else { 83 | panic!("wrong reply variant, expected GetAccountTransfers but found: {self:?}") 84 | } 85 | } 86 | 87 | pub fn into_lookup_accounts(self) -> Vec { 88 | if let Reply::LookupAccounts(out) = self { 89 | out 90 | } else { 91 | panic!("wrong reply variant, expected LookupAccounts but found: {self:?}") 92 | } 93 | } 94 | 95 | pub fn into_lookup_transfers(self) -> Vec { 96 | if let Reply::LookupTransfers(out) = self { 97 | out 98 | } else { 99 | panic!("wrong reply variant, expected LookupTransfers but found: {self:?}") 100 | } 101 | } 102 | 103 | pub fn into_query_accounts(self) -> Vec { 104 | if let Reply::QueryAccounts(out) = self { 105 | out 106 | } else { 107 | panic!("wrong reply variant, expected QueryAccounts but found: {self:?}") 108 | } 109 | } 110 | 111 | pub fn into_query_transfers(self) -> Vec { 112 | if let Reply::QueryTransfers(out) = self { 113 | out 114 | } else { 115 | panic!("wrong reply variant, expected QueryTransfers but found: {self:?}") 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /core/src/callback.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | marker::PhantomData, 3 | panic::catch_unwind, 4 | slice, 5 | time::{Duration, SystemTime}, 6 | }; 7 | 8 | use crate::util::RawConstPtr; 9 | 10 | use super::Packet; 11 | 12 | pub trait CallbacksPtr: RawConstPtr + callbacks_ptr::Sealed { 13 | type Pointee: Callbacks + Sized; 14 | type UserDataPtr: UserDataPtr; 15 | type UserData: UserData; 16 | } 17 | 18 | impl CallbacksPtr for T 19 | where 20 | T: RawConstPtr, 21 | T::Target: Callbacks + Sized, 22 | { 23 | type Pointee = T::Target; 24 | type UserDataPtr = ::UserDataPtr; 25 | type UserData = ::Pointee; 26 | } 27 | 28 | mod callbacks_ptr { 29 | use super::{Callbacks, RawConstPtr}; 30 | 31 | pub trait Sealed {} 32 | 33 | impl Sealed for T 34 | where 35 | T: RawConstPtr, 36 | T::Target: Callbacks + Sized, 37 | { 38 | } 39 | } 40 | 41 | /// Reply returned by [`Callbacks`]. 42 | #[non_exhaustive] 43 | pub struct Reply<'a> { 44 | /// Returned raw payload of this [`Reply`] as bytes. 45 | pub payload: &'a [u8], 46 | 47 | /// Cluster timestamp when the reply was generated. 48 | pub timestamp: SystemTime, 49 | } 50 | 51 | // `Self: Sync` because `F` is called from some Zig thread. 52 | pub trait Callbacks: Sync { 53 | type UserDataPtr: UserDataPtr; 54 | 55 | /// Calls back once a [`Packet`] is submitted and processed. 56 | /// 57 | /// [`None`] `reply` means that submitting the [`Packet`] failed (check the [`Packet::status`] 58 | /// for the reason). 59 | fn completion(&self, packet: Packet, reply: Option>); 60 | } 61 | 62 | pub struct CallbacksFn 63 | where 64 | F: Fn(Packet, Option>) + Sync, 65 | U: UserDataPtr, 66 | { 67 | inner: F, 68 | _marker: PhantomData, 69 | } 70 | 71 | impl CallbacksFn 72 | where 73 | F: Fn(Packet, Option>) + Sync, 74 | U: UserDataPtr, 75 | { 76 | pub const fn new(inner: F) -> Self 77 | where 78 | F: Sync, 79 | U: UserDataPtr, 80 | { 81 | Self { 82 | inner, 83 | _marker: PhantomData, 84 | } 85 | } 86 | } 87 | 88 | impl Callbacks for CallbacksFn 89 | where 90 | F: Fn(Packet, Option>) + Sync, 91 | U: UserDataPtr, 92 | { 93 | type UserDataPtr = U; 94 | 95 | fn completion(&self, packet: Packet, reply: Option>) { 96 | (self.inner)(packet, reply) 97 | } 98 | } 99 | 100 | pub const fn on_completion_fn(f: F) -> CallbacksFn 101 | where 102 | F: Fn(Packet, Option>) + Sync, 103 | U: UserDataPtr, 104 | { 105 | CallbacksFn::new(f) 106 | } 107 | 108 | pub(crate) unsafe extern "C" fn completion_callback_raw_fn( 109 | ctx: usize, 110 | packet: *mut sys::tb_packet_t, 111 | timestamp: u64, 112 | payload: *const u8, 113 | payload_size: u32, 114 | ) where 115 | F: Callbacks, 116 | { 117 | let _ = catch_unwind(|| { 118 | let cb = &*sptr::from_exposed_addr::(ctx); 119 | let payload_size = payload_size.try_into().expect( 120 | "at the start of calling `completion_callback`: \ 121 | unable to convert `payload_size` from `u32` into `usize`", 122 | ); 123 | let payload = if payload_size != 0 { 124 | slice::from_raw_parts(payload, payload_size) 125 | } else { 126 | &[] 127 | }; 128 | let packet = Packet { 129 | raw: packet, 130 | _ptr: PhantomData, 131 | }; 132 | let timestamp = SystemTime::UNIX_EPOCH + Duration::from_nanos(timestamp); 133 | cb.completion(packet, Some(Reply { payload, timestamp })) 134 | }); 135 | } 136 | 137 | // `Self: Send` because we are sending user_data into the callback as an 138 | // argument. 139 | pub trait UserDataPtr: RawConstPtr + Send + user_data_ptr::Sealed { 140 | type Pointee: UserData; 141 | } 142 | 143 | impl UserDataPtr for T 144 | where 145 | T: RawConstPtr + Send, 146 | T::Target: UserData + Sized, 147 | { 148 | type Pointee = T::Target; 149 | } 150 | 151 | mod user_data_ptr { 152 | use super::{RawConstPtr, UserData}; 153 | 154 | pub trait Sealed {} 155 | 156 | impl Sealed for T 157 | where 158 | T: RawConstPtr + Send, 159 | T::Target: UserData + Sized, 160 | { 161 | } 162 | } 163 | 164 | pub trait UserData { 165 | /// Borrow the data to send. 166 | fn data(&self) -> &[u8]; 167 | } 168 | -------------------------------------------------------------------------------- /core/src/util/raw_const_ptr.rs: -------------------------------------------------------------------------------- 1 | use std::{pin::Pin, rc::Rc, sync::Arc}; 2 | 3 | /// Trait that is used to generalize over various smart pointers. 4 | /// 5 | /// # Safety 6 | /// 7 | /// `into_raw_const_ptr` should pass ownership into the returned const pointer, 8 | /// while `from_raw_const_ptr` should regain ownership from a const pointer. 9 | pub unsafe trait RawConstPtr: std::ops::Deref { 10 | /// Pass ownership onto a returned const pointer. Returned pointer is 11 | /// non-null. 12 | /// 13 | /// You can safely immutably dereference returned pointer until 14 | /// you call `::from_raw_const_ptr` on it. 15 | /// Mutable dereferencing is allowed if `Self: DerefMut`. It is also alowed 16 | /// to create `Pin<&mut Self::Target>` if `Self` is `Pin

` 17 | /// where `P: DerefMut`. 18 | /// 19 | /// Dereference from other threads only if `Self::Target: Sync`. 20 | fn into_raw_const_ptr(this: Self) -> *const Self::Target; 21 | 22 | /// Regain ownership from a const pointer. 23 | /// 24 | /// # Safety 25 | /// 26 | /// You can only call this function once on a pointer returned from 27 | /// `::into_raw_const_ptr`. Can be called from other 28 | /// thread only if `Self: Send`. 29 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self; 30 | } 31 | 32 | unsafe impl RawConstPtr for Box { 33 | #[inline] 34 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 35 | Box::into_raw(this).cast_const() 36 | } 37 | 38 | #[inline] 39 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 40 | Box::from_raw(ptr.cast_mut()) 41 | } 42 | } 43 | 44 | unsafe impl RawConstPtr for Pin> { 45 | #[inline] 46 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 47 | Box::into_raw_const_ptr(unsafe { Pin::into_inner_unchecked(this) }) 48 | } 49 | 50 | #[inline] 51 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 52 | Pin::new_unchecked(Box::from_raw_const_ptr(ptr)) 53 | } 54 | } 55 | 56 | unsafe impl RawConstPtr for Arc { 57 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 58 | Arc::into_raw(this) 59 | } 60 | 61 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 62 | Arc::from_raw(ptr) 63 | } 64 | } 65 | 66 | unsafe impl RawConstPtr for Pin> { 67 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 68 | Arc::into_raw_const_ptr(unsafe { Pin::into_inner_unchecked(this) }) 69 | } 70 | 71 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 72 | Pin::new_unchecked(Arc::from_raw_const_ptr(ptr)) 73 | } 74 | } 75 | 76 | unsafe impl RawConstPtr for Rc { 77 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 78 | Rc::into_raw(this) 79 | } 80 | 81 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 82 | Rc::from_raw(ptr) 83 | } 84 | } 85 | 86 | unsafe impl RawConstPtr for Pin> { 87 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 88 | Rc::into_raw_const_ptr(unsafe { Pin::into_inner_unchecked(this) }) 89 | } 90 | 91 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 92 | Pin::new_unchecked(Rc::from_raw_const_ptr(ptr)) 93 | } 94 | } 95 | 96 | unsafe impl RawConstPtr for &T { 97 | #[inline] 98 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 99 | this as _ 100 | } 101 | 102 | #[inline] 103 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 104 | &*ptr 105 | } 106 | } 107 | 108 | unsafe impl RawConstPtr for Pin<&T> { 109 | #[inline] 110 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 111 | <&T>::into_raw_const_ptr(unsafe { Pin::into_inner_unchecked(this) }) 112 | } 113 | 114 | #[inline] 115 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 116 | Pin::new_unchecked(<&T>::from_raw_const_ptr(ptr)) 117 | } 118 | } 119 | 120 | unsafe impl RawConstPtr for &mut T { 121 | #[inline] 122 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 123 | (this as *mut Self::Target).cast_const() 124 | } 125 | 126 | #[inline] 127 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 128 | &mut *ptr.cast_mut() 129 | } 130 | } 131 | 132 | unsafe impl RawConstPtr for Pin<&mut T> { 133 | #[inline] 134 | fn into_raw_const_ptr(this: Self) -> *const Self::Target { 135 | <&mut T>::into_raw_const_ptr(unsafe { Pin::into_inner_unchecked(this) }) 136 | } 137 | 138 | #[inline] 139 | unsafe fn from_raw_const_ptr(ptr: *const Self::Target) -> Self { 140 | Pin::new_unchecked(<&mut T>::from_raw_const_ptr(ptr)) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /core/src/packet.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::c_void, marker::PhantomData, mem, num::NonZeroU8, ptr}; 2 | 3 | pub use sys::generated_safe::OperationKind; 4 | 5 | use super::callback::{UserData, UserDataPtr}; 6 | use crate::error::{sys_safe, SendError}; 7 | 8 | pub struct Packet 9 | where 10 | U: UserDataPtr, 11 | { 12 | pub(super) raw: *mut sys::tb_packet_t, 13 | pub(super) _ptr: PhantomData, 14 | } 15 | 16 | #[derive(Clone, Copy)] 17 | pub struct Operation(pub(crate) u8); 18 | 19 | unsafe impl Sync for Packet 20 | where 21 | U: UserDataPtr, 22 | U::Pointee: Sync, 23 | { 24 | } 25 | unsafe impl Send for Packet where U: UserDataPtr {} 26 | 27 | impl Packet 28 | where 29 | U: UserDataPtr, 30 | { 31 | /// Creates a new [`Packet`]. 32 | #[must_use] 33 | pub fn new(user_data: U, operation: impl Into) -> Self { 34 | Self { 35 | raw: Box::into_raw(Box::new(sys::tb_packet_t { 36 | user_data: U::into_raw_const_ptr(user_data).cast::().cast_mut(), 37 | data: ptr::null_mut(), 38 | data_size: 0, 39 | user_tag: 0, 40 | operation: operation.into().0, 41 | status: 0, 42 | opaque: [0; 64], 43 | })), 44 | _ptr: PhantomData, 45 | } 46 | } 47 | 48 | pub(crate) fn raw(&self) -> &sys::tb_packet_t { 49 | unsafe { &*self.raw } 50 | } 51 | 52 | pub(crate) fn raw_mut(&mut self) -> &mut sys::tb_packet_t { 53 | unsafe { &mut *self.raw } 54 | } 55 | 56 | pub fn into_user_data(self) -> U { 57 | let this = mem::ManuallyDrop::new(self); 58 | let user_data; 59 | unsafe { 60 | user_data = U::from_raw_const_ptr(this.raw().user_data.cast_const().cast()); 61 | drop(Box::from_raw(this.raw)); 62 | } 63 | user_data 64 | } 65 | 66 | pub fn replace_user_data(&mut self, user_data: U) -> U { 67 | let new = U::into_raw_const_ptr(user_data).cast_mut().cast(); 68 | let ptr = mem::replace(&mut self.raw_mut().user_data, new) 69 | .cast_const() 70 | .cast(); 71 | unsafe { U::from_raw_const_ptr(ptr) } 72 | } 73 | 74 | pub fn user_data(&self) -> &U::Target { 75 | unsafe { self.raw().user_data.cast::().as_ref().unwrap() } 76 | } 77 | 78 | pub fn user_data_mut(&mut self) -> &mut U::Target 79 | where 80 | U: std::ops::DerefMut, 81 | { 82 | unsafe { 83 | self.raw_mut() 84 | .user_data 85 | .cast::() 86 | .as_mut() 87 | .unwrap() 88 | } 89 | } 90 | 91 | pub fn data(&self) -> &[u8] { 92 | self.user_data().data() 93 | } 94 | 95 | pub fn operation(&self) -> Operation { 96 | Operation(self.raw().operation) 97 | } 98 | 99 | pub fn set_operation(&mut self, operation: Operation) { 100 | self.raw_mut().operation = operation.0; 101 | } 102 | 103 | pub fn status(&self) -> Result<(), SendError> { 104 | if let Some(c) = NonZeroU8::new(self.raw().status) { 105 | Err(SendError(c)) 106 | } else { 107 | Ok(()) 108 | } 109 | } 110 | 111 | pub fn set_status(&mut self, status: Result<(), SendError>) { 112 | self.raw_mut().status = match status { 113 | Ok(()) => 0, 114 | Err(e) => e.0.get(), 115 | } 116 | } 117 | } 118 | 119 | impl Drop for Packet 120 | where 121 | U: UserDataPtr, 122 | { 123 | fn drop(&mut self) { 124 | unsafe { 125 | drop(U::from_raw_const_ptr( 126 | self.raw().user_data.cast_const().cast(), 127 | )); 128 | drop(Box::from_raw(self.raw)); 129 | } 130 | } 131 | } 132 | 133 | impl Operation { 134 | const CODE_RANGE: std::ops::RangeInclusive = 135 | sys::generated_safe::MIN_OPERATION_CODE..=sys::generated_safe::MAX_OPERATION_CODE; 136 | 137 | pub fn kind(self) -> OperationKind { 138 | if Self::CODE_RANGE.contains(&self.0) 139 | && !sys_safe::EXCLUDED_OPERATION_CODES.contains(&self.0) 140 | { 141 | // SAFETY: We checked if it's in range right above. 142 | unsafe { mem::transmute::(self.0) } 143 | } else { 144 | OperationKind::UnstableUncategorized 145 | } 146 | } 147 | 148 | pub fn code(self) -> u8 { 149 | self.0 150 | } 151 | } 152 | 153 | impl std::fmt::Debug for Operation { 154 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 155 | let mut d = f.debug_tuple("Operation"); 156 | if Self::CODE_RANGE.contains(&self.0) { 157 | d.field(&self.kind()); 158 | } else { 159 | d.field(&self.0); 160 | } 161 | d.finish() 162 | } 163 | } 164 | 165 | impl From for Operation { 166 | /// Panics on hidden `OperationKind::UnstableUncategorized` variant. 167 | fn from(value: OperationKind) -> Self { 168 | let code = value as _; 169 | if !Self::CODE_RANGE.contains(&code) { 170 | panic!("OperationKind::{value:?}") 171 | } 172 | Operation(code) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /core/src/account.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use bytemuck::{Pod, TransparentWrapper, Zeroable}; 4 | 5 | mod balance; 6 | mod filter; 7 | 8 | pub use balance::{Balance, Raw as RawBalance}; 9 | pub use filter::{Filter, Flags as FilterFlags, Raw as RawFilter}; 10 | pub use sys::generated_safe::AccountFlags as Flags; 11 | pub use sys::tb_account_t as Raw; 12 | 13 | #[repr(transparent)] 14 | #[derive(Clone, Copy, TransparentWrapper, Pod, Zeroable)] 15 | pub struct Account(Raw); 16 | 17 | impl Account { 18 | #[track_caller] 19 | pub fn new(id: u128, ledger: u32, code: u16) -> Self { 20 | Account(Raw::zeroed()) 21 | .with_id(id) 22 | .with_ledger(ledger) 23 | .with_code(code) 24 | } 25 | 26 | pub const fn from_raw(raw: Raw) -> Self { 27 | Account(raw) 28 | } 29 | pub const fn into_raw(self) -> Raw { 30 | self.0 31 | } 32 | pub const fn as_raw(&self) -> &Raw { 33 | &self.0 34 | } 35 | pub fn as_raw_mut(&mut self) -> &mut Raw { 36 | &mut self.0 37 | } 38 | 39 | pub const fn id(&self) -> u128 { 40 | self.0.id 41 | } 42 | #[track_caller] 43 | pub fn set_id(&mut self, id: u128) { 44 | assert_ne!(id, 0, "account id must not be zero"); 45 | assert_ne!( 46 | id, 47 | u128::MAX, 48 | "account id must not be `2^128 - 1` (the highest 128-bit unsigned integer)" 49 | ); 50 | self.0.id = id; 51 | } 52 | #[track_caller] 53 | pub fn with_id(mut self, id: u128) -> Self { 54 | self.set_id(id); 55 | self 56 | } 57 | 58 | pub const fn user_data_128(&self) -> u128 { 59 | self.0.user_data_128 60 | } 61 | pub fn set_user_data_128(&mut self, user_data_128: u128) { 62 | self.0.user_data_128 = user_data_128; 63 | } 64 | pub const fn with_user_data_128(mut self, user_data_128: u128) -> Self { 65 | self.0.user_data_128 = user_data_128; 66 | self 67 | } 68 | 69 | pub const fn user_data_64(&self) -> u64 { 70 | self.0.user_data_64 71 | } 72 | pub fn set_user_data_64(&mut self, user_data_64: u64) { 73 | self.0.user_data_64 = user_data_64; 74 | } 75 | pub const fn with_user_data_64(mut self, user_data_64: u64) -> Self { 76 | self.0.user_data_64 = user_data_64; 77 | self 78 | } 79 | 80 | pub const fn user_data_32(&self) -> u32 { 81 | self.0.user_data_32 82 | } 83 | pub fn set_user_data_32(&mut self, user_data_32: u32) { 84 | self.0.user_data_32 = user_data_32; 85 | } 86 | pub const fn with_user_data_32(mut self, user_data_32: u32) -> Self { 87 | self.0.user_data_32 = user_data_32; 88 | self 89 | } 90 | 91 | pub const fn ledger(&self) -> u32 { 92 | self.0.ledger 93 | } 94 | #[track_caller] 95 | pub fn set_ledger(&mut self, ledger: u32) { 96 | assert_ne!(ledger, 0, "account ledger must not be zero"); 97 | self.0.ledger = ledger; 98 | } 99 | #[track_caller] 100 | pub fn with_ledger(mut self, ledger: u32) -> Self { 101 | self.set_ledger(ledger); 102 | self 103 | } 104 | 105 | pub const fn code(&self) -> u16 { 106 | self.0.code 107 | } 108 | #[track_caller] 109 | pub fn set_code(&mut self, code: u16) { 110 | assert_ne!(code, 0, "account code must not be zero"); 111 | self.0.code = code; 112 | } 113 | #[track_caller] 114 | pub fn with_code(mut self, code: u16) -> Self { 115 | self.set_code(code); 116 | self 117 | } 118 | 119 | pub const fn flags(&self) -> Flags { 120 | Flags::from_bits_retain(self.0.flags) 121 | } 122 | pub fn set_flags(&mut self, flags: Flags) { 123 | self.0.flags = flags.bits(); 124 | } 125 | pub const fn with_flags(mut self, flags: Flags) -> Self { 126 | self.0.flags = flags.bits(); 127 | self 128 | } 129 | 130 | pub const fn debits_pending(&self) -> u128 { 131 | self.0.debits_pending 132 | } 133 | pub const fn debits_posted(&self) -> u128 { 134 | self.0.debits_posted 135 | } 136 | pub const fn credits_pending(&self) -> u128 { 137 | self.0.credits_pending 138 | } 139 | pub const fn credits_posted(&self) -> u128 { 140 | self.0.credits_posted 141 | } 142 | 143 | pub fn timestamp(&self) -> SystemTime { 144 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp) 145 | } 146 | } 147 | 148 | impl std::fmt::Debug for Account { 149 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 150 | f.debug_struct("Account") 151 | .field("id", &self.id()) 152 | .field("debits_pending", &self.debits_pending()) 153 | .field("debits_posted", &self.debits_posted()) 154 | .field("credits_pending", &self.credits_pending()) 155 | .field("credits_posted", &self.credits_posted()) 156 | .field("user_data_128", &self.user_data_128()) 157 | .field("user_data_64", &self.user_data_64()) 158 | .field("user_data_32", &self.user_data_32()) 159 | .field("ledger", &self.ledger()) 160 | .field("code", &self.code()) 161 | .field("flags", &self.flags()) 162 | .field("timestamp", &self.timestamp()) 163 | .finish_non_exhaustive() 164 | } 165 | } 166 | 167 | impl From for Account { 168 | fn from(value: Raw) -> Self { 169 | Account(value) 170 | } 171 | } 172 | impl From for Raw { 173 | fn from(value: Account) -> Self { 174 | value.0 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/id.rs: -------------------------------------------------------------------------------- 1 | //! [TigerBeetle Time-Based Identifier][0] implementation. 2 | //! 3 | //! [0]: https://docs.tigerbeetle.com/coding/data-modeling#tigerbeetle-time-based-identifiers-recommended 4 | 5 | use std::{sync::Mutex, time::UNIX_EPOCH}; 6 | 7 | /// Returns the current timestamp in milliseconds since [`UNIX_EPOCH`]. 8 | /// 9 | /// # Panics 10 | /// 11 | /// - If the [`SystemTime`] clock went backwards beyond [`UNIX_EPOCH`]. 12 | /// - If milliseconds since [`UNIX_EPOCH`] overflow [`u64`]. 13 | fn get_current_timestamp() -> u64 { 14 | UNIX_EPOCH 15 | .elapsed() 16 | .unwrap_or_else(|e| panic!("`SystemTime` went backwards beyond `UNIX_EPOCH`: {e}")) 17 | .as_millis() 18 | .try_into() 19 | .unwrap_or_else(|e| panic!("milliseconds since `UNIX_EPOCH` overflow `u64`: {e}")) 20 | } 21 | 22 | /// Generates and returns 10 random bytes. 23 | fn generate_random_bytes() -> [u8; 10] { 24 | let mut bytes = [0u8; 10]; 25 | fastrand::fill(&mut bytes); 26 | bytes 27 | } 28 | 29 | /// Generates a new [TigerBeetle Time-Based Identifier][0]. 30 | /// 31 | /// [TigerBeetle Time-Based Identifier][0] consists of: 32 | /// - 48 bits of (millisecond) timestamp (high-order bits) 33 | /// - 80 bits of randomness (low-order bits) 34 | /// 35 | /// [0]: https://docs.tigerbeetle.com/coding/data-modeling#tigerbeetle-time-based-identifiers-recommended 36 | #[must_use] 37 | pub fn id() -> u128 { 38 | static LAST: Mutex<(u64, [u8; 10])> = Mutex::new((0, [0; 10])); 39 | 40 | let (timestamp, random) = { 41 | let timestamp = get_current_timestamp(); 42 | 43 | // Lock the `Mutex` to ensure that `last_timestamp` is monotonically increasing and 44 | // `last_random` changes each millisecond. 45 | let (last_timestamp, last_random) = &mut *LAST.lock().unwrap(); 46 | if timestamp > *last_timestamp { 47 | *last_timestamp = timestamp; 48 | *last_random = generate_random_bytes(); 49 | } 50 | 51 | // Read out a `u80` from the `last_random` as a `u64` and `u16`. 52 | // PANIC: Unwrapping is OK here, since `mem::size_of() == 8` and 53 | // `mem::size_of() == 2`. 54 | let mut random_lo = u64::from_le_bytes(last_random[..8].try_into().unwrap()); 55 | let mut random_hi = u16::from_le_bytes(last_random[8..].try_into().unwrap()); 56 | 57 | // Increment the random bits as a `u80` together, checking for overflow. 58 | random_lo = random_lo.wrapping_add(1); 59 | if random_lo == 0 { 60 | random_hi = random_hi.wrapping_add(1); 61 | if random_hi == 0 { 62 | *last_timestamp = last_timestamp.wrapping_add(1); 63 | } 64 | } 65 | 66 | // Write incremented `u80` back to the `last_random`. 67 | last_random[..8].copy_from_slice(&random_lo.to_le_bytes()); 68 | last_random[8..].copy_from_slice(&random_hi.to_le_bytes()); 69 | 70 | (*last_timestamp, *last_random) 71 | }; 72 | 73 | // Create `u128` from new `timestamp` and `random`. 74 | let mut id = [0u8; 16]; 75 | id[0..10].copy_from_slice(&random); 76 | id[10..16].copy_from_slice(×tamp.to_le_bytes()[..6]); 77 | u128::from_le_bytes(id) 78 | } 79 | 80 | #[cfg(test)] 81 | mod id_spec { 82 | use std::{sync::Barrier, thread, time::Duration}; 83 | 84 | use super::id; 85 | 86 | #[test] 87 | fn unique() { 88 | let id1 = id(); 89 | let id2 = id(); 90 | assert_ne!(id1, id2, "expected: {id1} != {id2}"); 91 | } 92 | 93 | #[test] 94 | fn monotonic_between_millis() { 95 | let id1 = id(); 96 | thread::sleep(Duration::from_millis(2)); 97 | let id2 = id(); 98 | assert!(id1 < id2, "expected: {id1} < {id2}"); 99 | } 100 | 101 | #[test] 102 | fn monotonic_within_millis() { 103 | let id1 = id(); 104 | thread::sleep(Duration::from_micros(1)); 105 | let id2 = id(); 106 | assert!(id1 < id2, "expected: {id1} < {id2}"); 107 | } 108 | 109 | #[test] 110 | fn monotonic_immediately() { 111 | let id1 = id(); 112 | let id2 = id(); 113 | assert!(id1 < id2, "expected: {id1} < {id2}"); 114 | } 115 | 116 | // Port of upstream test: 117 | // https://github.com/tigerbeetle/tigerbeetle/blob/0.16.67/src/clients/go/pkg/types/main_test.go#L92-L132 118 | #[test] 119 | fn monotonic_fuzz() { 120 | fn verifier() { 121 | let mut id1 = id(); 122 | for i in 0..1_000_000 { 123 | if i % 1_000 == 0 { 124 | thread::sleep(Duration::from_millis(1)); 125 | } 126 | let id2 = id(); 127 | 128 | assert!(id1 < id2, "expected: {id1} < {id2}"); 129 | 130 | id1 = id2; 131 | } 132 | } 133 | 134 | // Verify monotonic IDs locally. 135 | verifier(); 136 | 137 | // Verify monotonic IDs across multiple threads. 138 | let n = 10; 139 | let barrier = Barrier::new(n); 140 | thread::scope(|s| { 141 | let threads = (0..n) 142 | .map(|i| { 143 | thread::Builder::new() 144 | .name(i.to_string()) 145 | .spawn_scoped(s, || { 146 | // Sync up all threads before `verifier()` to maximize contention. 147 | barrier.wait(); 148 | verifier(); 149 | }) 150 | .unwrap() 151 | }) 152 | .collect::>(); 153 | for t in threads { 154 | t.join().unwrap(); 155 | } 156 | }); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /core/src/query_filter.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | time::{Duration, SystemTime}, 4 | }; 5 | 6 | use bytemuck::{Pod, TransparentWrapper, Zeroable}; 7 | 8 | pub use sys::generated_safe::QueryFilterFlags as Flags; 9 | pub use sys::tb_query_filter_t as Raw; 10 | 11 | #[derive(Clone, Copy, Pod, TransparentWrapper, Zeroable)] 12 | #[repr(transparent)] 13 | pub struct QueryFilter(Raw); 14 | 15 | impl fmt::Debug for QueryFilter { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.debug_struct("QueryFilter") 18 | .field("user_data_128", &self.0.user_data_128) 19 | .field("user_data_64", &self.0.user_data_64) 20 | .field("user_data_32", &self.0.user_data_32) 21 | .field("ledger", &self.0.ledger) 22 | .field("code", &self.0.code) 23 | .field("timestamp_min", &self.timestamp_min()) 24 | .field("timestamp_max", &self.timestamp_max()) 25 | .field("limit", &self.0.limit) 26 | .field("flags", &self.flags()) 27 | .finish_non_exhaustive() 28 | } 29 | } 30 | 31 | impl QueryFilter { 32 | #[track_caller] 33 | pub fn new(limit: u32) -> Self { 34 | Self(Raw::zeroed()).with_limit(limit) 35 | } 36 | 37 | pub const fn from_raw(raw: Raw) -> Self { 38 | Self(raw) 39 | } 40 | pub const fn into_raw(self) -> Raw { 41 | self.0 42 | } 43 | pub const fn as_raw(&self) -> &Raw { 44 | &self.0 45 | } 46 | pub fn as_raw_mut(&mut self) -> &mut Raw { 47 | &mut self.0 48 | } 49 | 50 | pub const fn user_data_128(&self) -> u128 { 51 | self.0.user_data_128 52 | } 53 | pub fn set_user_data_128(&mut self, user_data: u128) { 54 | self.0.user_data_128 = user_data; 55 | } 56 | pub fn with_user_data_128(mut self, user_data: u128) -> Self { 57 | self.set_user_data_128(user_data); 58 | self 59 | } 60 | 61 | pub const fn user_data_64(&self) -> u64 { 62 | self.0.user_data_64 63 | } 64 | pub fn set_user_data_64(&mut self, user_data: u64) { 65 | self.0.user_data_64 = user_data; 66 | } 67 | pub fn with_user_data_64(mut self, user_data: u64) -> Self { 68 | self.set_user_data_64(user_data); 69 | self 70 | } 71 | 72 | pub const fn user_data_32(&self) -> u32 { 73 | self.0.user_data_32 74 | } 75 | pub fn set_user_data_32(&mut self, user_data: u32) { 76 | self.0.user_data_32 = user_data; 77 | } 78 | pub fn with_user_data_32(mut self, user_data: u32) -> Self { 79 | self.set_user_data_32(user_data); 80 | self 81 | } 82 | 83 | pub const fn ledger(&self) -> u32 { 84 | self.0.ledger 85 | } 86 | pub fn set_ledger(&mut self, ledger: u32) { 87 | self.0.ledger = ledger; 88 | } 89 | pub fn with_ledger(mut self, ledger: u32) -> Self { 90 | self.set_ledger(ledger); 91 | self 92 | } 93 | 94 | pub const fn code(&self) -> u16 { 95 | self.0.code 96 | } 97 | pub fn set_code(&mut self, code: u16) { 98 | self.0.code = code; 99 | } 100 | pub fn with_code(mut self, code: u16) -> Self { 101 | self.set_code(code); 102 | self 103 | } 104 | 105 | pub fn timestamp_min(&self) -> SystemTime { 106 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp_min) 107 | } 108 | pub fn set_timestamp_min(&mut self, timestamp_min: SystemTime) { 109 | let t = timestamp_min 110 | .duration_since(SystemTime::UNIX_EPOCH) 111 | .ok() 112 | .and_then(|t| t.as_nanos().try_into().ok()) 113 | .expect("failed to get nanoseconds since unix epoch from the argument"); 114 | assert_ne!(t, u64::MAX, "timestamp_min must not be `2^64 - 1`"); 115 | self.0.timestamp_min = t; 116 | } 117 | pub fn with_timestamp_min(mut self, timestamp_min: SystemTime) -> Self { 118 | self.set_timestamp_min(timestamp_min); 119 | self 120 | } 121 | 122 | pub fn timestamp_max(&self) -> SystemTime { 123 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp_max) 124 | } 125 | pub fn set_timestamp_max(&mut self, timestamp_max: SystemTime) { 126 | let t = timestamp_max 127 | .duration_since(SystemTime::UNIX_EPOCH) 128 | .ok() 129 | .and_then(|t| t.as_nanos().try_into().ok()) 130 | .expect("failed to get nanoseconds since unix epoch from the argument"); 131 | assert_ne!(t, u64::MAX, "timestamp_max must not be `2^64 - 1`"); 132 | self.0.timestamp_max = t; 133 | } 134 | pub fn with_timestamp_max(mut self, timestamp_max: SystemTime) -> Self { 135 | self.set_timestamp_max(timestamp_max); 136 | self 137 | } 138 | 139 | pub const fn limit(&self) -> u32 { 140 | self.0.limit 141 | } 142 | pub fn set_limit(&mut self, limit: u32) { 143 | assert_ne!(limit, 0, "limit must not be zero"); 144 | self.0.limit = limit; 145 | } 146 | pub fn with_limit(mut self, limit: u32) -> Self { 147 | self.set_limit(limit); 148 | self 149 | } 150 | 151 | pub const fn flags(&self) -> Flags { 152 | Flags::from_bits_retain(self.0.flags) 153 | } 154 | pub fn set_flags(&mut self, flags: Flags) { 155 | self.0.flags = flags.bits(); 156 | } 157 | pub const fn with_flags(mut self, flags: Flags) -> Self { 158 | self.0.flags = flags.bits(); 159 | self 160 | } 161 | } 162 | 163 | impl From for QueryFilter { 164 | fn from(value: Raw) -> Self { 165 | Self(value) 166 | } 167 | } 168 | impl From for Raw { 169 | fn from(value: QueryFilter) -> Self { 170 | value.0 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /examples/c_port_high_level.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use tigerbeetle_unofficial as tb; 4 | 5 | // config.message_size_max - @sizeOf(vsr.Header): 6 | const MAX_MESSAGE_BYTE_SIZE: usize = (1024 * 1024) - 256; 7 | 8 | // Crate is runtime agnostic, so you can use tokio or any other async runtime 9 | #[pollster::main] 10 | async fn main() { 11 | println!("TigerBeetle C Sample"); 12 | println!("Connecting..."); 13 | let address = std::env::var("TB_ADDRESS"); 14 | let address = address.as_deref().unwrap_or("3000"); 15 | let client = tb::Client::new(0, address).expect("creating a tigerbeetle client"); 16 | 17 | //////////////////////////////////////////////////////////// 18 | // Submitting a batch of accounts: // 19 | //////////////////////////////////////////////////////////// 20 | 21 | println!("Creating accounts..."); 22 | let accounts = [ 23 | tb::Account::new(1, 777, 2).with_user_data_32(1), 24 | tb::Account::new(2, 777, 2), 25 | ]; 26 | client 27 | .create_accounts(accounts.to_vec()) 28 | .await 29 | .expect("creating accounts"); 30 | println!("Accounts created successfully"); 31 | 32 | //////////////////////////////////////////////////////////// 33 | // Submitting multiple batches of transfers: // 34 | //////////////////////////////////////////////////////////// 35 | 36 | println!("Creating transfers..."); 37 | const MAX_BATCHES: usize = 100; 38 | const TRANSFERS_PER_BATCH: usize = 39 | MAX_MESSAGE_BYTE_SIZE / std::mem::size_of::() - 1; 40 | let max_batches = std::env::var("TIGERBEETLE_RS_MAX_BATCHES") 41 | .ok() 42 | .and_then(|s| s.parse().ok()) 43 | .unwrap_or(MAX_BATCHES); 44 | let mut max_latency = Duration::ZERO; 45 | let mut total_time = Duration::ZERO; 46 | 47 | for i in 0..max_batches { 48 | let transfers: Vec<_> = (0..TRANSFERS_PER_BATCH) 49 | .map(|j| { 50 | tb::Transfer::new((j + 1 + i * TRANSFERS_PER_BATCH).try_into().unwrap()) 51 | .with_debit_account_id(accounts[0].id()) 52 | .with_credit_account_id(accounts[1].id()) 53 | .with_code(2) 54 | .with_ledger(777) 55 | .with_amount(1) 56 | .with_user_data_32(1) 57 | }) 58 | .collect(); 59 | 60 | let start = Instant::now(); 61 | client 62 | .create_transfers(transfers) 63 | .await 64 | .expect("creating transfers"); 65 | 66 | let elapsed = start.elapsed(); 67 | max_latency = max_latency.max(elapsed); 68 | total_time += elapsed; 69 | } 70 | 71 | println!("Transfers created successfully"); 72 | println!("============================================"); 73 | println!( 74 | "{:.0} transfers per second", 75 | (max_batches * TRANSFERS_PER_BATCH) as f64 / total_time.as_secs_f64() 76 | ); 77 | println!( 78 | "create_transfers max p100 latency per {} transfers = {}ms", 79 | TRANSFERS_PER_BATCH, 80 | max_latency.as_millis() 81 | ); 82 | println!( 83 | "total {} transfers in {}ms", 84 | max_batches * TRANSFERS_PER_BATCH, 85 | total_time.as_millis() 86 | ); 87 | println!(); 88 | 89 | //////////////////////////////////////////////////////////// 90 | // Looking up accounts: // 91 | //////////////////////////////////////////////////////////// 92 | 93 | println!("Looking up accounts ..."); 94 | let ids = accounts.map(|a| a.id()); 95 | let accounts = client 96 | .lookup_accounts(ids.to_vec()) 97 | .await 98 | .expect("looking up accounts"); 99 | assert!(!accounts.is_empty()); 100 | println!("{} Account(s) found", accounts.len()); 101 | println!("============================================"); 102 | for account in accounts { 103 | println!( 104 | "Account {{ id: {}, debits_posted: {}, credits_posted: {}, .. }}", 105 | account.id(), 106 | account.debits_posted(), 107 | account.credits_posted() 108 | ); 109 | } 110 | 111 | //////////////////////////////////////////////////////////// 112 | // Querying accounts: // 113 | //////////////////////////////////////////////////////////// 114 | 115 | println!("Querying accounts ..."); 116 | let accounts = client 117 | .query_accounts(Box::new( 118 | tb::QueryFilter::new(u32::MAX).with_user_data_32(1), 119 | )) 120 | .await 121 | .expect("querying accounts"); 122 | assert!(!accounts.is_empty()); 123 | println!("{} Account(s) found", accounts.len()); 124 | println!("============================================"); 125 | for account in accounts { 126 | println!( 127 | "Account {{ id: {}, debits_posted: {}, credits_posted: {}, .. }}", 128 | account.id(), 129 | account.debits_posted(), 130 | account.credits_posted() 131 | ); 132 | } 133 | 134 | //////////////////////////////////////////////////////////// 135 | // Querying transfers: // 136 | //////////////////////////////////////////////////////////// 137 | 138 | println!("Querying transfers ..."); 139 | let transfers = client 140 | .query_transfers(Box::new( 141 | tb::QueryFilter::new(u32::MAX).with_user_data_32(1), 142 | )) 143 | .await 144 | .expect("querying transfers"); 145 | assert!(!transfers.is_empty()); 146 | println!("{} Transfer(s) found", transfers.len()); 147 | println!("============================================"); 148 | for transfer in transfers { 149 | println!( 150 | "Transfer {{ id: {}, debit_account_id: {}, credit_account_id: {}, amount: {}, .. }}", 151 | transfer.id(), 152 | transfer.debit_account_id(), 153 | transfer.credit_account_id(), 154 | transfer.amount(), 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /core/src/transfer.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use bytemuck::{Pod, TransparentWrapper, Zeroable}; 4 | 5 | pub use sys::generated_safe::TransferFlags as Flags; 6 | pub use sys::tb_transfer_t as Raw; 7 | 8 | #[repr(transparent)] 9 | #[derive(Clone, Copy, TransparentWrapper, Pod, Zeroable)] 10 | pub struct Transfer(Raw); 11 | 12 | impl Transfer { 13 | #[track_caller] 14 | pub fn new(id: u128) -> Self { 15 | Transfer(Raw::zeroed()).with_id(id) 16 | } 17 | 18 | pub const fn from_raw(raw: Raw) -> Self { 19 | Transfer(raw) 20 | } 21 | pub const fn into_raw(self) -> Raw { 22 | self.0 23 | } 24 | pub const fn as_raw(&self) -> &Raw { 25 | &self.0 26 | } 27 | pub fn as_raw_mut(&mut self) -> &mut Raw { 28 | &mut self.0 29 | } 30 | 31 | pub const fn id(&self) -> u128 { 32 | self.0.id 33 | } 34 | #[track_caller] 35 | pub fn set_id(&mut self, id: u128) { 36 | assert_ne!(id, 0, "transfer id must not be zero"); 37 | assert_ne!( 38 | id, 39 | u128::MAX, 40 | "transfer id must not be `2^128 - 1` (the highest 128-bit unsigned integer)" 41 | ); 42 | self.0.id = id; 43 | } 44 | #[track_caller] 45 | pub fn with_id(mut self, id: u128) -> Self { 46 | self.set_id(id); 47 | self 48 | } 49 | 50 | pub const fn debit_account_id(&self) -> u128 { 51 | self.0.debit_account_id 52 | } 53 | pub fn set_debit_account_id(&mut self, debit_account_id: u128) { 54 | self.0.debit_account_id = debit_account_id; 55 | } 56 | pub const fn with_debit_account_id(mut self, debit_account_id: u128) -> Self { 57 | self.0.debit_account_id = debit_account_id; 58 | self 59 | } 60 | 61 | pub const fn credit_account_id(&self) -> u128 { 62 | self.0.credit_account_id 63 | } 64 | pub fn set_credit_account_id(&mut self, credit_account_id: u128) { 65 | self.0.credit_account_id = credit_account_id; 66 | } 67 | pub const fn with_credit_account_id(mut self, credit_account_id: u128) -> Self { 68 | self.0.credit_account_id = credit_account_id; 69 | self 70 | } 71 | 72 | pub const fn user_data_128(&self) -> u128 { 73 | self.0.user_data_128 74 | } 75 | pub fn set_user_data_128(&mut self, user_data_128: u128) { 76 | self.0.user_data_128 = user_data_128; 77 | } 78 | pub const fn with_user_data_128(mut self, user_data_128: u128) -> Self { 79 | self.0.user_data_128 = user_data_128; 80 | self 81 | } 82 | 83 | pub const fn user_data_64(&self) -> u64 { 84 | self.0.user_data_64 85 | } 86 | pub fn set_user_data_64(&mut self, user_data_64: u64) { 87 | self.0.user_data_64 = user_data_64; 88 | } 89 | pub const fn with_user_data_64(mut self, user_data_64: u64) -> Self { 90 | self.0.user_data_64 = user_data_64; 91 | self 92 | } 93 | 94 | pub const fn user_data_32(&self) -> u32 { 95 | self.0.user_data_32 96 | } 97 | pub fn set_user_data_32(&mut self, user_data_32: u32) { 98 | self.0.user_data_32 = user_data_32; 99 | } 100 | pub const fn with_user_data_32(mut self, user_data_32: u32) -> Self { 101 | self.0.user_data_32 = user_data_32; 102 | self 103 | } 104 | 105 | pub const fn ledger(&self) -> u32 { 106 | self.0.ledger 107 | } 108 | pub fn set_ledger(&mut self, ledger: u32) { 109 | self.0.ledger = ledger; 110 | } 111 | pub const fn with_ledger(mut self, ledger: u32) -> Self { 112 | self.0.ledger = ledger; 113 | self 114 | } 115 | 116 | pub const fn code(&self) -> u16 { 117 | self.0.code 118 | } 119 | pub fn set_code(&mut self, code: u16) { 120 | self.0.code = code; 121 | } 122 | pub const fn with_code(mut self, code: u16) -> Self { 123 | self.0.code = code; 124 | self 125 | } 126 | 127 | pub const fn pending_id(&self) -> u128 { 128 | self.0.pending_id 129 | } 130 | pub fn set_pending_id(&mut self, pending_id: u128) { 131 | self.0.pending_id = pending_id; 132 | } 133 | pub const fn with_pending_id(mut self, pending_id: u128) -> Self { 134 | self.0.pending_id = pending_id; 135 | self 136 | } 137 | 138 | pub const fn flags(&self) -> Flags { 139 | Flags::from_bits_retain(self.0.flags) 140 | } 141 | pub fn set_flags(&mut self, flags: Flags) { 142 | self.0.flags = flags.bits(); 143 | } 144 | pub const fn with_flags(mut self, flags: Flags) -> Self { 145 | self.0.flags = flags.bits(); 146 | self 147 | } 148 | 149 | pub const fn timeout(&self) -> u32 { 150 | self.0.timeout 151 | } 152 | #[track_caller] 153 | pub fn set_timeout(&mut self, timeout: u32) { 154 | self.0.timeout = timeout; 155 | } 156 | #[track_caller] 157 | pub fn with_timeout(mut self, timeout: u32) -> Self { 158 | self.set_timeout(timeout); 159 | self 160 | } 161 | 162 | pub const fn amount(&self) -> u128 { 163 | self.0.amount 164 | } 165 | pub fn set_amount(&mut self, amount: u128) { 166 | self.0.amount = amount; 167 | } 168 | pub const fn with_amount(mut self, amount: u128) -> Self { 169 | self.0.amount = amount; 170 | self 171 | } 172 | 173 | pub fn timestamp(&self) -> SystemTime { 174 | SystemTime::UNIX_EPOCH + Duration::from_nanos(self.0.timestamp) 175 | } 176 | } 177 | 178 | impl std::fmt::Debug for Transfer { 179 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 180 | f.debug_struct("Transfer") 181 | .field("id", &self.id()) 182 | .field("debit_account_id", &self.debit_account_id()) 183 | .field("credit_account_id", &self.credit_account_id()) 184 | .field("amount", &self.amount()) 185 | .field("pending_id", &self.pending_id()) 186 | .field("user_data_128", &self.user_data_128()) 187 | .field("user_data_64", &self.user_data_64()) 188 | .field("user_data_32", &self.user_data_32()) 189 | .field("timeout", &self.timeout()) 190 | .field("ledger", &self.ledger()) 191 | .field("code", &self.code()) 192 | .field("flags", &self.flags()) 193 | .field("timestamp", &self.timestamp()) 194 | .finish_non_exhaustive() 195 | } 196 | } 197 | 198 | impl From for Transfer { 199 | fn from(value: Raw) -> Self { 200 | Transfer(value) 201 | } 202 | } 203 | impl From for Raw { 204 | fn from(value: Transfer) -> Self { 205 | value.0 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc( 2 | html_logo_url = "https://avatars.githubusercontent.com/u/187310527", 3 | html_favicon_url = "https://avatars.githubusercontent.com/u/187310527?s=256" 4 | )] 5 | #![warn( 6 | clippy::match_wildcard_for_single_variants, 7 | clippy::wildcard_enum_match_arm 8 | )] 9 | 10 | pub mod account; 11 | mod callback; 12 | pub mod error; 13 | mod packet; 14 | pub mod query_filter; 15 | pub mod transfer; 16 | pub mod util; 17 | 18 | use std::{cell::UnsafeCell, marker::PhantomData, mem, num::NonZeroU32, pin::Pin}; 19 | 20 | use error::{NewClientError, NewClientErrorKind}; 21 | 22 | pub use account::Account; 23 | pub use callback::*; 24 | pub use packet::*; 25 | pub use query_filter::QueryFilter; 26 | pub use transfer::Transfer; 27 | 28 | type CompletionCallbackRawFn = 29 | unsafe extern "C" fn(usize, *mut sys::tb_packet_t, u64, *const u8, u32); 30 | 31 | pub struct Client 32 | where 33 | F: CallbacksPtr, 34 | { 35 | raw: Pin>>, 36 | cb: *const F::Target, 37 | marker: PhantomData, 38 | } 39 | 40 | unsafe impl Send for Client where F: CallbacksPtr + Send {} 41 | unsafe impl Sync for Client where F: CallbacksPtr {} 42 | 43 | impl Client 44 | where 45 | F: CallbacksPtr, 46 | { 47 | pub fn with_callback( 48 | cluster_id: u128, 49 | address: A, 50 | completion_callback: F, 51 | ) -> Result 52 | where 53 | A: AsRef<[u8]>, 54 | // `F` and `UserDataPtr` are `'static`, because we can `mem::forget(self)` 55 | // and drop anything that is being referred from `F` or `UserDataPtr`, 56 | // thus invalidating callback or user data. 57 | F: 'static, 58 | F::UserDataPtr: 'static, 59 | { 60 | // SAFETY: `F` and `UserDataPtr` are `'static`. 61 | unsafe { Client::with_callback_unchecked(cluster_id, address, completion_callback) } 62 | } 63 | 64 | /// Highly unsafe method. Please use [`Self::with_callback`] 65 | /// unless you are *really sure* you are doing it right. 66 | /// 67 | /// # Safety 68 | /// 69 | /// `F` and `U` are unresticted by any lifetime. It's user's responsibility 70 | /// to ensure validity of `on_completion` callback or packet's `user_data` 71 | /// for client's use. If client is dropped, you can safely invalidate these 72 | /// things. 73 | pub unsafe fn with_callback_unchecked( 74 | cluster_id: u128, 75 | address: A, 76 | completion_callback: F, 77 | ) -> Result 78 | where 79 | A: AsRef<[u8]>, 80 | { 81 | let completion_fn = completion_callback_raw_fn::; 82 | let completion_cb = F::into_raw_const_ptr(completion_callback); 83 | let completion_ctx = sptr::Strict::expose_addr(completion_cb); 84 | 85 | unsafe fn raw_with_callback( 86 | cluster_id: u128, 87 | address: &[u8], 88 | completion_ctx: usize, 89 | completion_callback: CompletionCallbackRawFn, 90 | ) -> Result>>, NewClientError> { 91 | let mut raw = Box::pin(UnsafeCell::new(mem::zeroed())); 92 | let status = sys::tb_client_init( 93 | raw.as_mut().get_unchecked_mut().get_mut(), 94 | cluster_id.to_le_bytes().as_ptr(), 95 | address.as_ptr().cast(), 96 | address 97 | .len() 98 | .try_into() 99 | .map_err(|_| NewClientErrorKind::AddressInvalid)?, 100 | completion_ctx, 101 | Some(completion_callback), 102 | ); 103 | 104 | // SAFETY: Unwrapping is OK here, because the returned `TB_INIT_STATUS` is actually an 105 | // enum with positive discriminant undoubtedly fitting into `u32`. 106 | #[allow(clippy::useless_conversion)] // not true for Windows 107 | if let Some(c) = NonZeroU32::new(status.try_into().unwrap_unchecked()) { 108 | Err(NewClientError(c)) 109 | } else { 110 | Ok(raw) 111 | } 112 | } 113 | 114 | Ok(Client { 115 | raw: unsafe { 116 | raw_with_callback(cluster_id, address.as_ref(), completion_ctx, completion_fn) 117 | .inspect_err(|_| drop(F::from_raw_const_ptr(completion_cb)))? 118 | }, 119 | cb: completion_cb, 120 | marker: PhantomData, 121 | }) 122 | } 123 | 124 | pub fn submit(&self, mut packet: Packet) { 125 | use crate::error::SendErrorKind; 126 | 127 | let data = packet.user_data().data(); 128 | let Ok(data_size) = data.len().try_into() else { 129 | packet.set_status(Err(SendErrorKind::TooMuchData.into())); 130 | let cb = unsafe { &*self.cb }; 131 | cb.completion(packet, None); 132 | return; 133 | }; 134 | let data = data.as_ptr(); 135 | 136 | let raw_packet = packet.raw_mut(); 137 | raw_packet.data_size = data_size; 138 | raw_packet.data = data.cast_mut().cast(); 139 | 140 | // SAFETY: Going from `&self` to `*mut sys::tb_client_t` is OK here, because multi-thread 141 | // access is synchronized by the `sys::tb_client_t` itself inside. 142 | unsafe { 143 | let raw_client: *mut sys::tb_client_t = self.raw.get(); 144 | // NOTE: We do omit checking the result to be `TB_CLIENT_INVALID` intentionally here, 145 | // because it can be returned only if the `raw_client` is not yet inited, or was 146 | // deinited already, which happens only in constructors and `Drop` respectively. 147 | _ = sys::tb_client_submit(raw_client, packet.raw); 148 | } 149 | mem::forget(packet); // avoid `Drop`ping `Packet` 150 | } 151 | } 152 | 153 | /// Blocks until all pending requests finish 154 | impl Drop for Client 155 | where 156 | F: CallbacksPtr, 157 | { 158 | fn drop(&mut self) { 159 | unsafe { 160 | let raw_client = self.raw.as_mut().get_unchecked_mut().get_mut(); 161 | #[cfg(feature = "tokio-rt-multi-thread")] 162 | if tokio::runtime::Handle::try_current().is_ok_and(|h| { 163 | matches!( 164 | h.runtime_flavor(), 165 | tokio::runtime::RuntimeFlavor::MultiThread 166 | ) 167 | }) { 168 | _ = tokio::task::block_in_place(|| sys::tb_client_deinit(raw_client)); 169 | } else { 170 | _ = sys::tb_client_deinit(raw_client); 171 | } 172 | #[cfg(not(feature = "tokio-rt-multi-thread"))] 173 | { 174 | _ = sys::tb_client_deinit(raw_client); 175 | } 176 | 177 | drop(F::from_raw_const_ptr(self.cb)); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc( 2 | html_logo_url = "https://avatars.githubusercontent.com/u/187310527", 3 | html_favicon_url = "https://avatars.githubusercontent.com/u/187310527?s=256" 4 | )] 5 | #![forbid(unsafe_code)] 6 | 7 | mod id; 8 | mod reply; 9 | 10 | use error::NewClientError; 11 | use reply::Reply; 12 | use tokio::sync::oneshot; 13 | 14 | use core::{ 15 | error::{CreateAccountsError, CreateTransfersError, SendError}, 16 | util::{RawConstPtr, SendAsBytesOwnedSlice, SendOwnedSlice}, 17 | }; 18 | 19 | pub use core::{self, account, error, transfer, Account, Packet, QueryFilter, Transfer}; 20 | 21 | pub use self::id::id; 22 | 23 | pub struct Client { 24 | inner: core::Client<&'static Callbacks>, 25 | } 26 | 27 | struct Callbacks; 28 | 29 | struct UserData { 30 | reply_sender: oneshot::Sender>, 31 | data: SendAsBytesOwnedSlice, 32 | } 33 | 34 | impl Client { 35 | pub fn new(cluster_id: u128, address: A) -> Result 36 | where 37 | A: AsRef<[u8]>, 38 | { 39 | Ok(Client { 40 | inner: core::Client::with_callback(cluster_id, address, &Callbacks)?, 41 | }) 42 | } 43 | 44 | pub async fn create_accounts(&self, accounts: T) -> Result<(), CreateAccountsError> 45 | where 46 | T: Into>, 47 | { 48 | let accounts: SendOwnedSlice = accounts.into(); 49 | if accounts.is_empty() { 50 | return Ok(()); 51 | } 52 | Ok(self 53 | .submit( 54 | accounts.into_as_bytes(), 55 | core::OperationKind::CreateAccounts, 56 | ) 57 | .await? 58 | .into_create_accounts()?) 59 | } 60 | 61 | pub async fn create_transfers(&self, transfers: T) -> Result<(), CreateTransfersError> 62 | where 63 | T: Into>, 64 | { 65 | let transfers: SendOwnedSlice = transfers.into(); 66 | if transfers.is_empty() { 67 | return Ok(()); 68 | } 69 | Ok(self 70 | .submit( 71 | transfers.into_as_bytes(), 72 | core::OperationKind::CreateTransfers, 73 | ) 74 | .await? 75 | .into_create_transfers()?) 76 | } 77 | 78 | pub async fn get_account_balances( 79 | &self, 80 | filter: T, 81 | ) -> Result, SendError> 82 | where 83 | T: RawConstPtr + Send + 'static, 84 | { 85 | let filter: SendOwnedSlice = SendOwnedSlice::from_single(filter); 86 | self.submit( 87 | filter.into_as_bytes(), 88 | core::OperationKind::GetAccountBalances, 89 | ) 90 | .await 91 | .map(Reply::into_get_account_balances) 92 | } 93 | 94 | pub async fn get_account_transfers(&self, filter: T) -> Result, SendError> 95 | where 96 | T: RawConstPtr + Send + 'static, 97 | { 98 | let filter: SendOwnedSlice = SendOwnedSlice::from_single(filter); 99 | self.submit( 100 | filter.into_as_bytes(), 101 | core::OperationKind::GetAccountTransfers, 102 | ) 103 | .await 104 | .map(Reply::into_get_account_transfers) 105 | } 106 | 107 | pub async fn lookup_accounts(&self, ids: T) -> Result, SendError> 108 | where 109 | T: Into>, 110 | { 111 | let ids: SendOwnedSlice = ids.into(); 112 | if ids.is_empty() { 113 | return Ok(Vec::new()); 114 | } 115 | self.submit(ids.into_as_bytes(), core::OperationKind::LookupAccounts) 116 | .await 117 | .map(Reply::into_lookup_accounts) 118 | } 119 | 120 | pub async fn lookup_transfers(&self, ids: T) -> Result, SendError> 121 | where 122 | T: Into>, 123 | { 124 | let ids: SendOwnedSlice = ids.into(); 125 | if ids.is_empty() { 126 | return Ok(Vec::new()); 127 | } 128 | self.submit(ids.into_as_bytes(), core::OperationKind::LookupTransfers) 129 | .await 130 | .map(Reply::into_lookup_transfers) 131 | } 132 | 133 | pub async fn query_accounts(&self, filter: T) -> Result, SendError> 134 | where 135 | T: RawConstPtr + Send + 'static, 136 | { 137 | let filter: SendOwnedSlice = SendOwnedSlice::from_single(filter); 138 | self.submit(filter.into_as_bytes(), core::OperationKind::QueryAccounts) 139 | .await 140 | .map(Reply::into_query_accounts) 141 | } 142 | 143 | pub async fn query_transfers(&self, filter: T) -> Result, SendError> 144 | where 145 | T: RawConstPtr + Send + 'static, 146 | { 147 | let filter: SendOwnedSlice = SendOwnedSlice::from_single(filter); 148 | self.submit(filter.into_as_bytes(), core::OperationKind::QueryTransfers) 149 | .await 150 | .map(Reply::into_query_transfers) 151 | } 152 | 153 | async fn submit( 154 | &self, 155 | data: SendAsBytesOwnedSlice, 156 | operation: impl Into, 157 | ) -> Result { 158 | let (reply_sender, reply_receiver) = oneshot::channel(); 159 | let user_data = Box::new(UserData { reply_sender, data }); 160 | self.inner.submit(Packet::new(user_data, operation)); 161 | reply_receiver.await.unwrap() 162 | } 163 | } 164 | 165 | impl core::Callbacks for Callbacks { 166 | type UserDataPtr = Box; 167 | 168 | fn completion(&self, packet: Packet, reply: Option>) { 169 | let status = packet.status(); 170 | let operation = packet.operation(); 171 | let user_data = packet.into_user_data(); 172 | // Channel may be closed due to the `Future` cancellation, so the `.send()` error should 173 | // be ignored. 174 | user_data 175 | .reply_sender 176 | .send(status.map(|()| { 177 | // PANIC: Unwrapping is OK here, because the `reply` can only be `None` when the 178 | // `status` is `Err`. 179 | Reply::copy_from_reply(operation.kind(), reply.unwrap().payload) 180 | })) 181 | .unwrap_or_else(drop); 182 | } 183 | } 184 | 185 | impl core::UserData for UserData { 186 | fn data(&self) -> &[u8] { 187 | self.data.as_ref() 188 | } 189 | } 190 | 191 | fn _test_thread_safe( 192 | client: Client, 193 | accounts: Vec, 194 | transfers: Vec, 195 | query_filter: &'static QueryFilter, 196 | account_filter: &'static account::Filter, 197 | ids: Vec, 198 | ) { 199 | check_thread_safe(async move { 200 | client.create_accounts(accounts).await.unwrap(); 201 | client.create_transfers(transfers).await.unwrap(); 202 | client.get_account_balances(account_filter).await.unwrap(); 203 | client.get_account_transfers(account_filter).await.unwrap(); 204 | client.lookup_accounts(ids.clone()).await.unwrap(); 205 | client.lookup_transfers(ids).await.unwrap(); 206 | client.query_accounts(query_filter).await.unwrap(); 207 | client.query_transfers(query_filter).await.unwrap(); 208 | }); 209 | 210 | fn check_thread_safe(_: T) 211 | where 212 | T: Send + Sync + 'static, 213 | { 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: ["main"] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | RUST_BACKTRACE: 1 16 | 17 | jobs: 18 | 19 | ################ 20 | # Pull Request # 21 | ################ 22 | 23 | pr: 24 | if: ${{ always() && github.event_name == 'pull_request' }} 25 | needs: 26 | - clippy 27 | - example 28 | - msrv 29 | - rustfmt 30 | - test 31 | runs-on: ubuntu-latest 32 | steps: 33 | - run: ${{ needs.clippy.result == 'success' 34 | && needs.example.result == 'success' 35 | && needs.msrv.result == 'success' 36 | && needs.rustfmt.result == 'success' 37 | && needs.test.result == 'success' }} 38 | 39 | 40 | 41 | 42 | ########################## 43 | # Linting and formatting # 44 | ########################## 45 | 46 | clippy: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v6 50 | with: 51 | submodules: true 52 | - uses: dtolnay/rust-toolchain@v1 53 | with: 54 | toolchain: stable 55 | components: clippy,rustfmt 56 | 57 | - run: cargo clippy --workspace --all-features -- -D warnings 58 | 59 | rustfmt: 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v6 63 | - uses: dtolnay/rust-toolchain@v1 64 | with: 65 | toolchain: nightly 66 | components: rustfmt 67 | 68 | - run: cargo +nightly fmt --all -- --check 69 | 70 | 71 | 72 | 73 | ########### 74 | # Testing # 75 | ########### 76 | 77 | example: 78 | name: example (${{ matrix.example }}) 79 | strategy: 80 | fail-fast: false 81 | matrix: 82 | include: 83 | - example: c_port_low_level 84 | crate: tigerbeetle-unofficial-core 85 | - example: c_port_high_level 86 | crate: tigerbeetle-unofficial 87 | runs-on: ubuntu-latest 88 | steps: 89 | - uses: actions/checkout@v6 90 | with: 91 | submodules: true 92 | - uses: dtolnay/rust-toolchain@v1 93 | with: 94 | toolchain: stable 95 | components: rustfmt 96 | 97 | - name: Parse TigerBeetle version from Cargo manifest 98 | id: tigerbeetle 99 | run: echo "version=$(grep -m1 -e '^version = "' Cargo.toml | cut -d'"' -f2 | cut -d'+' -f2)" 100 | >> $GITHUB_OUTPUT 101 | 102 | - name: Start TigerBeetle server 103 | run: docker run --rm -d --name tigerbeetle -p 3000:3000 104 | --security-opt seccomp=unconfined --cap-add IPC_LOCK 105 | --entrypoint sh ghcr.io/tigerbeetle/tigerbeetle:${{ steps.tigerbeetle.outputs.version }} -c 106 | '/tigerbeetle format --cluster=0 --replica=0 --replica-count=1 /db.tb 107 | && exec /tigerbeetle start --addresses=0.0.0.0:3000 /db.tb' 108 | 109 | - run: cargo run -p ${{ matrix.crate }} --example ${{ matrix.example }} 110 | 111 | - name: Stop TigerBeetle server 112 | run: docker stop tigerbeetle || true 113 | if: ${{ always() }} 114 | 115 | msrv: 116 | name: MSRV 117 | strategy: 118 | fail-fast: false 119 | matrix: 120 | msrv: ["1.78.0"] 121 | os: ["ubuntu", "macOS", "windows"] 122 | runs-on: ${{ matrix.os }}-latest 123 | steps: 124 | - uses: actions/checkout@v6 125 | with: 126 | submodules: true 127 | - uses: dtolnay/rust-toolchain@v1 128 | with: 129 | toolchain: nightly 130 | - uses: dtolnay/rust-toolchain@v1 131 | with: 132 | toolchain: ${{ matrix.msrv }}${{ matrix.os == 'windows' && '-gnu' || '' }} 133 | components: rustfmt 134 | 135 | - run: cargo +nightly update -Z minimal-versions 136 | 137 | - run: cargo test -p tigerbeetle-unofficial-sys --all-features 138 | 139 | - run: cargo test -p tigerbeetle-unofficial-core --all-features 140 | 141 | - run: cargo test -p tigerbeetle-unofficial --all-features 142 | 143 | test: 144 | strategy: 145 | fail-fast: false 146 | matrix: 147 | toolchain: ["stable", "beta", "nightly"] 148 | os: ["ubuntu", "macOS", "windows"] 149 | runs-on: ${{ matrix.os }}-latest 150 | steps: 151 | - uses: actions/checkout@v6 152 | with: 153 | submodules: true 154 | - uses: dtolnay/rust-toolchain@v1 155 | with: 156 | toolchain: ${{ matrix.toolchain }}${{ matrix.os == 'windows' && '-gnu' || '' }} 157 | components: rust-src,rustfmt 158 | 159 | - run: cargo install cargo-careful 160 | if: ${{ matrix.toolchain == 'nightly' && matrix.os != 'windows' }} 161 | 162 | - run: cargo ${{ (matrix.toolchain == 'nightly' && matrix.os != 'windows') && 'careful' || '' }} test 163 | -p tigerbeetle-unofficial-sys --all-features 164 | 165 | - run: cargo ${{ (matrix.toolchain == 'nightly' && matrix.os != 'windows') && 'careful' || '' }} test 166 | -p tigerbeetle-unofficial-core --all-features 167 | 168 | - run: cargo ${{ (matrix.toolchain == 'nightly' && matrix.os != 'windows') && 'careful' || '' }} test 169 | -p tigerbeetle-unofficial --all-features 170 | 171 | 172 | 173 | 174 | ############# 175 | # Releasing # 176 | ############# 177 | 178 | publish: 179 | name: publish (crates.io) 180 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 181 | needs: ["release-github"] 182 | runs-on: ubuntu-latest 183 | steps: 184 | - uses: actions/checkout@v6 185 | with: 186 | submodules: true 187 | - uses: dtolnay/rust-toolchain@v1 188 | with: 189 | toolchain: stable 190 | components: rustfmt 191 | 192 | # TODO: Investigate how to fix this. 193 | # `--no-verify` is required because the source directory is modified by `build.rs` (`zig/download.sh` running) 194 | # during `cargo publish`, which isn't happy about (build scripts should not modify anything outside of `OUT_DIR`). 195 | - run: cargo publish -p tigerbeetle-unofficial-sys --no-verify 196 | env: 197 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }} 198 | 199 | - run: cargo publish -p tigerbeetle-unofficial-core 200 | env: 201 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }} 202 | 203 | - run: cargo publish -p tigerbeetle-unofficial 204 | env: 205 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }} 206 | 207 | release-github: 208 | name: release (GitHub) 209 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 210 | needs: 211 | - clippy 212 | - example 213 | - msrv 214 | - rustfmt 215 | - test 216 | runs-on: ubuntu-latest 217 | steps: 218 | - uses: actions/checkout@v6 219 | 220 | - name: Parse release version 221 | id: release 222 | run: echo "version=${GITHUB_REF#refs/tags/v}" 223 | >> $GITHUB_OUTPUT 224 | - name: Verify release version matches Cargo manifest 225 | run: | 226 | test "${{ steps.release.outputs.version }}" \ 227 | == "$(grep -m1 -e '^version = "' Cargo.toml | cut -d'"' -f2)" 228 | 229 | - name: Ensure CHANGELOG date is today 230 | run: | 231 | today="$(date '+%Y-%m-%d')" 232 | changelog="$(grep -e '^## \[${{ steps.release.outputs.version }}\] ·' \ 233 | CHANGELOG.md \ 234 | | cut -d' ' -f4 | tr -d ' ')" 235 | echo "Changelog: $changelog" 236 | echo "Today: $today" 237 | [ "$changelog" = "$today" ] 238 | - name: Parse CHANGELOG link 239 | id: changelog 240 | run: echo "link=${{ github.server_url }}/${{ github.repository }}/blob/v${{ steps.release.outputs.version }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.release.outputs.version }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' CHANGELOG.md)" 241 | >> $GITHUB_OUTPUT 242 | 243 | - name: Create GitHub release 244 | uses: softprops/action-gh-release@v2 245 | with: 246 | name: ${{ steps.release.outputs.version }} 247 | body: | 248 | [API docs](https://docs.rs/tigerbeetle-unofficial/${{ steps.release.outputs.version }}) 249 | [Changelog](${{ steps.changelog.outputs.link }}) 250 | prerelease: ${{ contains(steps.release.outputs.version, '-') }} 251 | -------------------------------------------------------------------------------- /core/src/util/owned_slice.rs: -------------------------------------------------------------------------------- 1 | use std::{marker::PhantomData, mem, ptr::NonNull}; 2 | 3 | use super::{send_marker, RawConstPtr, SendMarker}; 4 | 5 | /// Generalized version of `Vec`, `Arc<[T]>`, `Box<[T]>`, `Rc<[T]>` or other 6 | /// sequential containers where `T` is an element type. Has transformation from 7 | /// these types. 8 | /// 9 | /// `S` is marker type for `Send` trait. If it set to [`send_marker::Sendable`], then 10 | /// [`OwnedSlice`] implements `Send`, or [`send_marker::Unsendable`] for `!Send`. 11 | /// There is [`SendOwnedSlice`] shortcut if you want sendable owned slice. 12 | pub struct OwnedSlice 13 | where 14 | S: SendMarker, 15 | { 16 | ptr: NonNull, 17 | /// Some slice containers may allow only invariance over elements `T` 18 | marker: PhantomData<(fn(T), S)>, 19 | ctx: SliceHandleContext, 20 | } 21 | 22 | pub type SendOwnedSlice = OwnedSlice; 23 | 24 | #[derive(Clone, Copy)] 25 | struct SliceHandleContext { 26 | len: usize, 27 | addend: usize, 28 | drop: unsafe fn(NonNull, usize, usize), 29 | } 30 | 31 | pub struct AsBytesOwnedSlice 32 | where 33 | S: SendMarker, 34 | { 35 | owner: OwnedSlice, 36 | size_of_element: usize, 37 | } 38 | 39 | pub type SendAsBytesOwnedSlice = AsBytesOwnedSlice; 40 | 41 | pub struct Erased(()); 42 | 43 | unsafe impl Send for SendOwnedSlice {} 44 | 45 | /// Do not put constrain over `M` cause we can always send reference to a `Sync` 46 | /// type to another thread. 47 | unsafe impl Sync for OwnedSlice 48 | where 49 | T: Sync, 50 | S: SendMarker, 51 | { 52 | } 53 | 54 | impl AsRef<[T]> for OwnedSlice 55 | where 56 | S: SendMarker, 57 | { 58 | #[inline] 59 | fn as_ref(&self) -> &[T] { 60 | self.as_slice() 61 | } 62 | } 63 | 64 | impl OwnedSlice 65 | where 66 | S: SendMarker, 67 | { 68 | /// Create owned slice from raw parts given the safety requirements. Used 69 | /// to implement `From` on `OwnedSlice` 70 | /// 71 | /// # Safety 72 | /// 73 | /// User must ensure that it is safe to create a immutible slice reference 74 | /// from `ptr` and `len`. `drop` should be safe to call with arguments 75 | /// `ptr.cast::<()>()`, `len` and `addend`. Use `SendOwnedSlice` or set 76 | /// type parameter `S = Sendable`, to indicate that original container can 77 | /// be sended to another thread to be dropped there. 78 | #[inline] 79 | pub unsafe fn from_raw_parts( 80 | ptr: NonNull, 81 | len: usize, 82 | addend: usize, 83 | drop: unsafe fn(NonNull, usize, usize), 84 | ) -> Self { 85 | OwnedSlice { 86 | ptr, 87 | marker: PhantomData, 88 | ctx: SliceHandleContext { len, addend, drop }, 89 | } 90 | } 91 | 92 | #[inline] 93 | pub fn as_slice(&self) -> &[T] { 94 | unsafe { NonNull::slice_from_raw_parts(self.ptr, self.ctx.len).as_ref() } 95 | } 96 | 97 | #[inline] 98 | pub fn len(&self) -> usize { 99 | self.ctx.len 100 | } 101 | 102 | #[inline] 103 | pub fn is_empty(&self) -> bool { 104 | self.ctx.len == 0 105 | } 106 | 107 | /// Get erased over `T` owned slice. Lifetimes of `self.as_ref()` references 108 | /// could be safely extended until owned slice is dropped. 109 | pub fn erase_type(self) -> OwnedSlice { 110 | let this = mem::ManuallyDrop::new(self); 111 | OwnedSlice { 112 | ptr: this.ptr.cast(), 113 | marker: PhantomData, 114 | ctx: this.ctx, 115 | } 116 | } 117 | 118 | /// Get owned slice with ability to inspect it's bytes. 119 | pub fn into_as_bytes(self) -> AsBytesOwnedSlice 120 | where 121 | T: bytemuck::NoUninit, 122 | { 123 | AsBytesOwnedSlice { 124 | owner: self.erase_type(), 125 | size_of_element: mem::size_of::(), 126 | } 127 | } 128 | } 129 | 130 | impl From> for OwnedSlice { 131 | fn from(value: SendOwnedSlice) -> Self { 132 | let value = mem::ManuallyDrop::new(value); 133 | OwnedSlice { 134 | ptr: value.ptr, 135 | marker: PhantomData, 136 | ctx: value.ctx, 137 | } 138 | } 139 | } 140 | 141 | impl From> for SendOwnedSlice { 142 | fn from(value: Vec) -> Self { 143 | unsafe fn drop_impl(ptr: NonNull, length: usize, capacity: usize) { 144 | Vec::from_raw_parts(ptr.cast::().as_ptr(), length, capacity); 145 | } 146 | let mut v = mem::ManuallyDrop::new(value); 147 | let len = v.len(); 148 | let capacity = v.capacity(); 149 | unsafe { 150 | OwnedSlice::from_raw_parts( 151 | NonNull::new_unchecked(v.as_mut_ptr()), 152 | len, 153 | capacity, 154 | drop_impl::, 155 | ) 156 | } 157 | } 158 | } 159 | 160 | impl From

for SendOwnedSlice 161 | where 162 | P: RawConstPtr + Send + 'static, 163 | { 164 | fn from(value: P) -> Self { 165 | unsafe fn drop_impl(ptr: NonNull, len: usize, _: usize) 166 | where 167 | P: RawConstPtr + 'static, 168 | { 169 | drop(P::from_raw_const_ptr( 170 | NonNull::slice_from_raw_parts(ptr.cast::(), len) 171 | .as_ptr() 172 | .cast_const(), 173 | )); 174 | } 175 | let ptr = unsafe { NonNull::new_unchecked(P::into_raw_const_ptr(value).cast_mut()) }; 176 | unsafe { OwnedSlice::from_raw_parts(ptr.cast(), ptr.len(), 0, drop_impl::) } 177 | } 178 | } 179 | 180 | impl SendOwnedSlice { 181 | pub fn from_single

(value: P) -> Self 182 | where 183 | P: RawConstPtr + Send + 'static, 184 | { 185 | unsafe fn drop_impl(ptr: NonNull, _: usize, _: usize) 186 | where 187 | P: RawConstPtr + 'static, 188 | { 189 | drop(P::from_raw_const_ptr(ptr.as_ptr() as *const T)); 190 | } 191 | let ptr = unsafe { NonNull::new_unchecked(P::into_raw_const_ptr(value).cast_mut()) }; 192 | unsafe { OwnedSlice::from_raw_parts(ptr.cast(), 1, 0, drop_impl::) } 193 | } 194 | } 195 | 196 | impl From

for OwnedSlice 197 | where 198 | P: RawConstPtr + 'static, 199 | { 200 | fn from(value: P) -> Self { 201 | unsafe fn drop_impl(ptr: NonNull, len: usize, _: usize) 202 | where 203 | P: RawConstPtr + 'static, 204 | { 205 | drop(P::from_raw_const_ptr( 206 | NonNull::slice_from_raw_parts(ptr.cast::(), len) 207 | .as_ptr() 208 | .cast_const(), 209 | )); 210 | } 211 | let ptr = unsafe { NonNull::new_unchecked(P::into_raw_const_ptr(value).cast_mut()) }; 212 | unsafe { OwnedSlice::from_raw_parts(ptr.cast(), ptr.len(), 0, drop_impl::) } 213 | } 214 | } 215 | impl OwnedSlice { 216 | pub fn from_single

(value: P) -> Self 217 | where 218 | P: RawConstPtr + 'static, 219 | { 220 | unsafe fn drop_impl(ptr: NonNull, _: usize, _: usize) 221 | where 222 | P: RawConstPtr + 'static, 223 | { 224 | drop(P::from_raw_const_ptr(ptr.as_ptr() as *const T)); 225 | } 226 | let ptr = unsafe { NonNull::new_unchecked(P::into_raw_const_ptr(value).cast_mut()) }; 227 | unsafe { OwnedSlice::from_raw_parts(ptr.cast(), 1, 0, drop_impl::) } 228 | } 229 | } 230 | 231 | impl Drop for OwnedSlice 232 | where 233 | S: SendMarker, 234 | { 235 | #[inline] 236 | fn drop(&mut self) { 237 | unsafe { (self.ctx.drop)(self.ptr.cast(), self.ctx.len, self.ctx.addend) } 238 | } 239 | } 240 | 241 | impl AsRef<[u8]> for AsBytesOwnedSlice 242 | where 243 | S: SendMarker, 244 | { 245 | #[inline] 246 | fn as_ref(&self) -> &[u8] { 247 | self.as_bytes() 248 | } 249 | } 250 | 251 | impl AsBytesOwnedSlice 252 | where 253 | S: SendMarker, 254 | { 255 | #[inline] 256 | pub fn as_bytes(&self) -> &[u8] { 257 | unsafe { 258 | NonNull::slice_from_raw_parts( 259 | self.owner.ptr.cast(), 260 | self.size_of_element * self.owner.ctx.len, 261 | ) 262 | .as_ref() 263 | } 264 | } 265 | 266 | #[inline] 267 | pub fn len(&self) -> usize { 268 | self.owner.len() 269 | } 270 | 271 | #[inline] 272 | pub fn is_empty(&self) -> bool { 273 | self.owner.is_empty() 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /core/examples/c_port_low_level.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Write, 3 | mem, 4 | sync::{Condvar, Mutex, MutexGuard}, 5 | time::{Duration, Instant}, 6 | }; 7 | 8 | use tigerbeetle_unofficial_core as tb; 9 | 10 | // config.message_size_max - @sizeOf(vsr.Header): 11 | const MAX_MESSAGE_SIZE: usize = (1024 * 1024) - 256; 12 | 13 | struct Callbacks; 14 | 15 | struct UserData { 16 | ctx: &'static CompletionContext, 17 | data: [u8; MAX_MESSAGE_SIZE], 18 | data_size: usize, 19 | } 20 | 21 | // Synchronization context between the callback and the main thread. 22 | // In this example we synchronize using a condition variable. 23 | struct CompletionContext { 24 | state: Mutex, 25 | cv: Condvar, 26 | } 27 | 28 | struct CompletionState { 29 | reply: [u8; MAX_MESSAGE_SIZE], 30 | size: usize, 31 | completed: Option<(Box, Result<(), tb::error::SendError>)>, 32 | } 33 | 34 | fn main() { 35 | println!("TigerBeetle C Sample"); 36 | println!("Connecting..."); 37 | let address = std::env::var("TB_ADDRESS"); 38 | let address = address.as_deref().unwrap_or("3000"); 39 | let client = tb::Client::with_callback(0, address.as_bytes(), &Callbacks) 40 | .expect("Failed to initialize tigerbeetle client"); 41 | 42 | static CTX: CompletionContext = CompletionContext::new(); 43 | 44 | //////////////////////////////////////////////////////////// 45 | // Submitting a batch of accounts: // 46 | //////////////////////////////////////////////////////////// 47 | 48 | let accounts = [ 49 | tb::Account::new(1, 777, 2).with_user_data_32(1), 50 | tb::Account::new(2, 777, 2), 51 | ]; 52 | let mut user_data = Box::new(UserData { 53 | ctx: &CTX, 54 | data: [0; MAX_MESSAGE_SIZE], 55 | data_size: 0, 56 | }); 57 | user_data.set_data(accounts); 58 | let mut packet = tb::Packet::new(user_data, tb::OperationKind::CreateAccounts); 59 | println!("Creating accounts..."); 60 | let mut state = CTX.state.lock().unwrap(); 61 | (user_data, state) = CTX.send_request(state, &client, packet).unwrap(); 62 | state.create_accounts_status().unwrap(); 63 | 64 | println!("Accounts created successfully"); 65 | 66 | //////////////////////////////////////////////////////////// 67 | // Submitting multiple batches of transfers: // 68 | //////////////////////////////////////////////////////////// 69 | 70 | println!("Creating transfers..."); 71 | const MAX_BATCHES: usize = 100; 72 | const TRANSFERS_PER_BATCH: usize = MAX_MESSAGE_SIZE / mem::size_of::() - 1; 73 | let max_batches = std::env::var("TIGERBEETLE_RS_MAX_BATCHES") 74 | .ok() 75 | .and_then(|s| s.parse().ok()) 76 | .unwrap_or(MAX_BATCHES); 77 | let mut max_latency = Duration::ZERO; 78 | let mut total_time = Duration::ZERO; 79 | 80 | for i in 0..max_batches { 81 | let transfers = (0..TRANSFERS_PER_BATCH).map(|j| { 82 | tb::Transfer::new((j + 1 + (i * TRANSFERS_PER_BATCH)) as u128) 83 | .with_debit_account_id(accounts[0].id()) 84 | .with_credit_account_id(accounts[1].id()) 85 | .with_code(2) 86 | .with_ledger(777) 87 | .with_amount(1) 88 | .with_user_data_32(1) 89 | }); 90 | user_data.set_data(transfers); 91 | packet = tb::Packet::new(user_data, tb::OperationKind::CreateTransfers); 92 | 93 | let now = Instant::now(); 94 | (user_data, state) = CTX.send_request(state, &client, packet).unwrap(); 95 | let elapsed = now.elapsed(); 96 | max_latency = max_latency.max(elapsed); 97 | total_time += elapsed; 98 | 99 | state.create_transfers_status().unwrap(); 100 | } 101 | 102 | println!("Transfers created successfully"); 103 | println!("============================================"); 104 | 105 | println!( 106 | "{} transfers per second\n", 107 | (max_batches * TRANSFERS_PER_BATCH * 1000) 108 | / usize::try_from(total_time.as_millis()).unwrap() 109 | ); 110 | println!( 111 | "create_transfers max p100 latency per {} transfers = {}ms", 112 | TRANSFERS_PER_BATCH, 113 | max_latency.as_millis() 114 | ); 115 | println!( 116 | "total {} transfers in {}ms", 117 | max_batches * TRANSFERS_PER_BATCH, 118 | total_time.as_millis() 119 | ); 120 | println!(); 121 | 122 | //////////////////////////////////////////////////////////// 123 | // Looking up accounts: // 124 | //////////////////////////////////////////////////////////// 125 | 126 | println!("Looking up accounts ..."); 127 | let ids = accounts.map(|a| a.id()); 128 | user_data.set_data(ids); 129 | packet = tb::Packet::new(user_data, tb::OperationKind::LookupAccounts); 130 | (user_data, state) = CTX.send_request(state, &client, packet).unwrap(); 131 | let accounts = state.get_data::(); 132 | if accounts.is_empty() { 133 | panic!("No accounts found"); 134 | } 135 | 136 | // Printing the accounts: 137 | println!("{} Account(s) found", accounts.len()); 138 | println!("============================================"); 139 | println!("{accounts:#?}"); 140 | 141 | //////////////////////////////////////////////////////////// 142 | // Querying accounts: // 143 | //////////////////////////////////////////////////////////// 144 | 145 | println!("Querying accounts ..."); 146 | user_data.set_data([tb::QueryFilter::new(u32::MAX).with_user_data_32(1)]); 147 | packet = tb::Packet::new(user_data, tb::OperationKind::QueryAccounts); 148 | (user_data, state) = CTX.send_request(state, &client, packet).unwrap(); 149 | let accounts = state.get_data::(); 150 | if accounts.is_empty() { 151 | panic!("No accounts found"); 152 | } 153 | 154 | // Printing the accounts: 155 | println!("{} Account(s) found", accounts.len()); 156 | println!("============================================"); 157 | println!("{accounts:#?}"); 158 | 159 | //////////////////////////////////////////////////////////// 160 | // Querying transfers: // 161 | //////////////////////////////////////////////////////////// 162 | 163 | println!("Querying transfers ..."); 164 | user_data.set_data([tb::QueryFilter::new(u32::MAX).with_user_data_32(1)]); 165 | packet = tb::Packet::new(user_data, tb::OperationKind::QueryTransfers); 166 | (_, state) = CTX.send_request(state, &client, packet).unwrap(); 167 | let transfers = state.get_data::(); 168 | if transfers.is_empty() { 169 | panic!("No transfers found"); 170 | } 171 | 172 | // Printing the transfers: 173 | println!("{} Transfer(s) found", transfers.len()); 174 | println!("============================================"); 175 | println!("{transfers:#?}"); 176 | } 177 | 178 | impl CompletionContext { 179 | const fn new() -> Self { 180 | CompletionContext { 181 | state: Mutex::new(CompletionState { 182 | reply: [0; MAX_MESSAGE_SIZE], 183 | size: 0, 184 | completed: None, 185 | }), 186 | cv: Condvar::new(), 187 | } 188 | } 189 | 190 | fn send_request<'a>( 191 | &self, 192 | mut guard: MutexGuard<'a, CompletionState>, 193 | client: &tb::Client<&Callbacks>, 194 | packet: tb::Packet>, 195 | ) -> Result<(Box, MutexGuard<'a, CompletionState>), tb::error::SendError> { 196 | guard.completed = None; 197 | client.submit(packet); 198 | loop { 199 | guard = self.cv.wait(guard).unwrap(); 200 | 201 | if let Some(c) = guard.completed.take() { 202 | break c.1.map(|()| (c.0, guard)); 203 | } 204 | } 205 | } 206 | } 207 | 208 | impl CompletionState { 209 | fn create_accounts_status(&self) -> Result<(), tb::error::CreateAccountsApiError> { 210 | match tb::error::CreateAccountsApiError::from_raw_results(self.get_data()) { 211 | Some(e) => Err(e), 212 | None => Ok(()), 213 | } 214 | } 215 | 216 | fn create_transfers_status(&self) -> Result<(), tb::error::CreateTransfersApiError> { 217 | match tb::error::CreateTransfersApiError::from_raw_results(self.get_data()) { 218 | Some(e) => Err(e), 219 | None => Ok(()), 220 | } 221 | } 222 | 223 | fn get_data(&self) -> Vec 224 | where 225 | T: bytemuck::Pod, 226 | { 227 | if self.size == 0 { 228 | return Vec::new(); 229 | } 230 | assert_eq!(self.size % mem::size_of::(), 0); 231 | let mut res = vec![T::zeroed(); self.size / mem::size_of::()]; 232 | bytemuck::cast_slice_mut::<_, u8>(&mut res).copy_from_slice(&self.reply[..self.size]); 233 | res 234 | } 235 | } 236 | 237 | impl tb::UserData for UserData { 238 | fn data(&self) -> &[u8] { 239 | &self.data[..self.data_size] 240 | } 241 | } 242 | 243 | impl UserData { 244 | fn set_data(&mut self, src: I) 245 | where 246 | I: IntoIterator, 247 | I::Item: bytemuck::Pod, 248 | { 249 | let mut dst = self.data.as_mut_slice(); 250 | for src in src { 251 | dst.write_all(bytemuck::bytes_of(&src)).unwrap(); 252 | } 253 | self.data_size = MAX_MESSAGE_SIZE - dst.len(); 254 | } 255 | 256 | fn free(self: Box, status: Result<(), tb::error::SendError>) { 257 | let mut l = self.ctx.state.lock().unwrap(); 258 | l.completed = Some((self, status)); 259 | } 260 | } 261 | 262 | impl tb::Callbacks for Callbacks { 263 | type UserDataPtr = Box; 264 | 265 | fn completion(&self, packet: tb::Packet, reply: Option>) { 266 | let status = packet.status(); 267 | let user_data = packet.into_user_data(); 268 | let ctx = user_data.ctx; 269 | if let Some(reply) = reply { 270 | let mut state = ctx.state.lock().unwrap(); 271 | state.reply[..reply.payload.len()].copy_from_slice(reply.payload); 272 | state.size = reply.payload.len(); 273 | } 274 | ctx.cv.notify_one(); 275 | user_data.free(status); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /core/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt, mem, 4 | num::{NonZeroU32, NonZeroU8}, 5 | }; 6 | 7 | pub use sys::{ 8 | generated_safe::{ 9 | self as sys_safe, CreateAccountErrorKind, CreateTransferErrorKind, 10 | InitStatusErrorKind as NewClientErrorKind, PacketStatusErrorKind as SendErrorKind, 11 | }, 12 | tb_create_accounts_result_t as RawCreateAccountsIndividualApiResult, 13 | tb_create_transfers_result_t as RawCreateTransfersIndividualApiResult, 14 | }; 15 | 16 | #[derive(Clone, Copy)] 17 | pub struct NewClientError(pub(crate) NonZeroU32); 18 | 19 | impl NewClientError { 20 | const CODE_RANGE: std::ops::RangeInclusive = 21 | sys_safe::MIN_INIT_STATUS_ERROR_CODE..=sys_safe::MAX_INIT_STATUS_ERROR_CODE; 22 | 23 | pub fn kind(self) -> NewClientErrorKind { 24 | let code = self.0.get(); 25 | if Self::CODE_RANGE.contains(&code) { 26 | // SAFETY: We checked if it's in range right above. 27 | unsafe { mem::transmute::(code) } 28 | } else { 29 | NewClientErrorKind::UnstableUncategorized 30 | } 31 | } 32 | 33 | pub fn code(self) -> NonZeroU32 { 34 | self.0 35 | } 36 | } 37 | 38 | impl fmt::Debug for NewClientError { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | let mut d = f.debug_tuple("NewClientError"); 41 | let kind = self.kind(); 42 | if matches!(kind, NewClientErrorKind::UnstableUncategorized) { 43 | let code = self.code(); 44 | d.field(&code); 45 | } else { 46 | d.field(&kind); 47 | } 48 | d.finish() 49 | } 50 | } 51 | 52 | impl fmt::Display for NewClientError { 53 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 54 | use NewClientErrorKind as K; 55 | 56 | match self.kind() { 57 | K::AddressInvalid => "Replica addresses format is invalid", 58 | K::AddressLimitExceeded => "Replica addresses limit exceeded", 59 | K::NetworkSubsystem => "Internal client had unexpected networking issues", 60 | K::OutOfMemory => "Internal client ran out of memory", 61 | K::SystemResources => "Internal client ran out of system resources", 62 | K::Unexpected => "Unexpected internal error", 63 | _ => return write!(f, "Unknown error status: {}", self.code()), 64 | } 65 | .fmt(f) 66 | } 67 | } 68 | 69 | impl Error for NewClientError {} 70 | 71 | impl From for NewClientError { 72 | /// Constructs a [`NewClientError`] out of the provided [`NewClientErrorKind`]. 73 | /// 74 | /// # Panics 75 | /// 76 | /// Panics on the hidden [`NewClientErrorKind::UnstableUncategorized`] variant. 77 | fn from(value: NewClientErrorKind) -> Self { 78 | let this = Self(NonZeroU32::new(value as _).unwrap()); 79 | if matches!(this.kind(), NewClientErrorKind::UnstableUncategorized) { 80 | panic!("NewClientErrorKind::{value:?}") 81 | } 82 | this 83 | } 84 | } 85 | 86 | #[derive(Clone, Copy)] 87 | pub struct SendError(pub(crate) NonZeroU8); 88 | 89 | impl SendError { 90 | const CODE_RANGE: std::ops::RangeInclusive = 91 | sys_safe::MIN_PACKET_STATUS_ERROR_CODE..=sys_safe::MAX_PACKET_STATUS_ERROR_CODE; 92 | 93 | pub fn kind(self) -> SendErrorKind { 94 | let code = self.0.get(); 95 | if Self::CODE_RANGE.contains(&code) { 96 | // SAFETY: We checked if it's in range right above. 97 | unsafe { mem::transmute::(code) } 98 | } else { 99 | SendErrorKind::UnstableUncategorized 100 | } 101 | } 102 | 103 | pub fn code(self) -> NonZeroU8 { 104 | self.0 105 | } 106 | } 107 | 108 | impl fmt::Debug for SendError { 109 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 110 | let mut d = f.debug_tuple("SendError"); 111 | let kind = self.kind(); 112 | if matches!(kind, SendErrorKind::UnstableUncategorized) { 113 | let code = self.code(); 114 | d.field(&code); 115 | } else { 116 | d.field(&kind); 117 | } 118 | d.finish() 119 | } 120 | } 121 | 122 | impl fmt::Display for SendError { 123 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 124 | use SendErrorKind as K; 125 | 126 | match self.kind() { 127 | K::TooMuchData => "Too much data provided on this batch", 128 | K::InvalidOperation => "Invalid operation", 129 | K::InvalidDataSize => "Invalid data size", 130 | K::ClientEvicted => "Client was evicted", 131 | K::ClientReleaseTooLow => "Client was evicted: release too old", 132 | K::ClientReleaseTooHigh => "Client was evicted: release too new", 133 | K::ClientShutdown => "Client was closed", 134 | _ => return write!(f, "Unknown error status: {}", self.code()), 135 | } 136 | .fmt(f) 137 | } 138 | } 139 | 140 | impl Error for SendError {} 141 | 142 | impl From for SendError { 143 | /// Constructs a [`SendError`] out of the provided [`SendErrorKind`]. 144 | /// 145 | /// # Panics 146 | /// 147 | /// Panics on the hidden [`SendErrorKind::UnstableUncategorized`] variant. 148 | fn from(value: SendErrorKind) -> Self { 149 | let this = Self(NonZeroU8::new(value as _).unwrap()); 150 | if matches!(this.kind(), SendErrorKind::UnstableUncategorized) { 151 | panic!("SendErrorKind::{value:?}") 152 | } 153 | this 154 | } 155 | } 156 | 157 | #[derive(Clone, Copy)] 158 | pub struct CreateAccountError(pub(crate) NonZeroU32); 159 | 160 | impl CreateAccountError { 161 | const CODE_RANGE: std::ops::RangeInclusive = 162 | sys_safe::MIN_CREATE_ACCOUNT_ERROR_CODE..=sys_safe::MAX_CREATE_ACCOUNT_ERROR_CODE; 163 | 164 | pub fn kind(self) -> CreateAccountErrorKind { 165 | let code = self.0.get(); 166 | if Self::CODE_RANGE.contains(&code) { 167 | // SAFETY: We checked if it's in range right above. 168 | unsafe { mem::transmute::(code) } 169 | } else { 170 | CreateAccountErrorKind::UnstableUncategorized 171 | } 172 | } 173 | 174 | pub fn code(self) -> NonZeroU32 { 175 | self.0 176 | } 177 | } 178 | 179 | impl fmt::Debug for CreateAccountError { 180 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 181 | let mut d = f.debug_tuple("CreateAccountError"); 182 | let kind = self.kind(); 183 | if matches!(kind, CreateAccountErrorKind::UnstableUncategorized) { 184 | d.field(&self.0); 185 | } else { 186 | d.field(&kind); 187 | } 188 | d.finish() 189 | } 190 | } 191 | 192 | impl fmt::Display for CreateAccountError { 193 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 194 | write!(f, "{:?}", self.kind()) 195 | } 196 | } 197 | 198 | impl Error for CreateAccountError {} 199 | 200 | impl From for CreateAccountError { 201 | /// Constructs a [`CreateAccountError`] out of the provided [`CreateAccountErrorKind`]. 202 | /// 203 | /// # Panics 204 | /// 205 | /// Panics on the hidden [`CreateAccountErrorKind::UnstableUncategorized`] variant. 206 | fn from(value: CreateAccountErrorKind) -> Self { 207 | let this = Self(NonZeroU32::new(value as _).unwrap()); 208 | if matches!(this.kind(), CreateAccountErrorKind::UnstableUncategorized) { 209 | panic!("CreateAccountErrorKind::{value:?}") 210 | } 211 | this 212 | } 213 | } 214 | 215 | /// Type indicating individual API error for account creation. 216 | /// 217 | /// Safe to `transpose` from [`RawCreateAccountsIndividualApiResult`] 218 | /// if [`Self::from_raw_result_unchecked`] would also be safe. 219 | // INVARIANT: `self.0.result` must not be zero. 220 | #[derive(Clone, Copy)] 221 | #[repr(transparent)] 222 | pub struct CreateAccountsIndividualApiError(RawCreateAccountsIndividualApiResult); 223 | 224 | impl CreateAccountsIndividualApiError { 225 | /// Create error from raw result. 226 | /// 227 | /// # Errors 228 | /// 229 | /// Returns `None` if `raw.result` is zero. 230 | pub fn from_raw_result(raw: RawCreateAccountsIndividualApiResult) -> Option { 231 | (raw.result != 0).then_some(Self(raw)) 232 | } 233 | 234 | /// Create error from raw result. Unchecked version of [`Self::from_raw_result`]. 235 | /// 236 | /// # Safety 237 | /// 238 | /// This function is unsafe. `raw.result` must not be zero. 239 | pub unsafe fn from_raw_result_unchecked(raw: RawCreateAccountsIndividualApiResult) -> Self { 240 | Self(raw) 241 | } 242 | 243 | /// Create vec of errors from vec of raw results. 244 | /// 245 | /// Retains only elements `r` of vec `v` that satisfy `r.result != 0`. 246 | pub fn vec_from_raw_results(mut v: Vec) -> Vec { 247 | v.retain(|r| r.result != 0); 248 | unsafe { Self::vec_from_raw_results_unchecked(v) } 249 | } 250 | 251 | /// Create vec of errors from vec of raw results. Unchecked version of 252 | /// [`Self::vec_from_raw_results`] 253 | /// 254 | /// # Safety 255 | /// 256 | /// This function is unsafe. Every element `r` of vec `v` must satisfy 257 | /// `r.result != 0`. 258 | pub unsafe fn vec_from_raw_results_unchecked( 259 | v: Vec, 260 | ) -> Vec { 261 | let mut v = mem::ManuallyDrop::new(v); 262 | let len = v.len(); 263 | let cap = v.capacity(); 264 | let ptr = v.as_mut_ptr().cast::(); 265 | // SAFETY: this is fine because `Vec::from_raw_parts` has pretty loose 266 | // safety requirements, and since `CreateAccountsIndividualApiError` is 267 | // just a transparent wrapper of `RawCreateAccountsIndividualApiResult` 268 | // this is safe. 269 | Vec::from_raw_parts(ptr, len, cap) 270 | } 271 | 272 | /// Get index of the failed account. 273 | pub fn index(&self) -> u32 { 274 | self.0.index 275 | } 276 | 277 | /// Get error stripped of context, like index. 278 | pub fn inner(&self) -> CreateAccountError { 279 | CreateAccountError( 280 | // SAFETY: type invariant 281 | unsafe { NonZeroU32::new_unchecked(self.0.result) }, 282 | ) 283 | } 284 | 285 | /// Get kind of error to match upon. 286 | pub fn kind(&self) -> CreateAccountErrorKind { 287 | self.inner().kind() 288 | } 289 | } 290 | 291 | impl fmt::Debug for CreateAccountsIndividualApiError { 292 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 293 | f.debug_struct("CreateAccountsIndividualApiError") 294 | .field("index", &self.index()) 295 | .field("inner", &self.inner()) 296 | .finish() 297 | } 298 | } 299 | 300 | impl fmt::Display for CreateAccountsIndividualApiError { 301 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 302 | write!( 303 | f, 304 | "`{}` error occurred at account with index {}", 305 | self.inner(), 306 | self.index(), 307 | ) 308 | } 309 | } 310 | 311 | impl Error for CreateAccountsIndividualApiError {} 312 | 313 | // INVARIANT: `self.0` must not be empty. 314 | #[derive(Debug)] 315 | pub struct CreateAccountsApiError(Vec); 316 | 317 | impl CreateAccountsApiError { 318 | /// Get a slice of individual errors. Never empty. 319 | pub fn as_slice(&self) -> &[CreateAccountsIndividualApiError] { 320 | &self.0 321 | } 322 | 323 | /// Create error from vec of raw results. 324 | /// 325 | /// # Errors 326 | /// 327 | /// Returns `None` if `v.is_empty()`. 328 | pub fn from_errors(v: Vec) -> Option { 329 | (!v.is_empty()).then_some(CreateAccountsApiError(v)) 330 | } 331 | 332 | /// Create error from vec of raw results. 333 | /// 334 | /// Retains only results with errors. 335 | pub fn from_raw_results(v: Vec) -> Option { 336 | Self::from_errors(CreateAccountsIndividualApiError::vec_from_raw_results(v)) 337 | } 338 | } 339 | 340 | impl AsRef<[CreateAccountsIndividualApiError]> for CreateAccountsApiError { 341 | fn as_ref(&self) -> &[CreateAccountsIndividualApiError] { 342 | &self.0 343 | } 344 | } 345 | 346 | impl fmt::Display for CreateAccountsApiError { 347 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 348 | write!( 349 | f, 350 | "{} api errors occurred at accounts' creation", 351 | self.0.len(), 352 | ) 353 | } 354 | } 355 | 356 | impl Error for CreateAccountsApiError { 357 | fn source(&self) -> Option<&(dyn Error + 'static)> { 358 | self.0.first().map(|e| e as _) 359 | } 360 | } 361 | 362 | impl From for CreateAccountsApiError { 363 | fn from(value: CreateAccountsIndividualApiError) -> Self { 364 | CreateAccountsApiError(vec![value]) 365 | } 366 | } 367 | 368 | #[derive(Debug)] 369 | #[non_exhaustive] 370 | pub enum CreateAccountsError { 371 | Send(SendError), 372 | Api(CreateAccountsApiError), 373 | } 374 | 375 | impl Error for CreateAccountsError { 376 | fn source(&self) -> Option<&(dyn Error + 'static)> { 377 | Some(match self { 378 | Self::Send(e) => e as _, 379 | Self::Api(e) => e as _, 380 | }) 381 | } 382 | } 383 | 384 | impl fmt::Display for CreateAccountsError { 385 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 386 | write!(f, "Failed to create accounts: ")?; 387 | match self { 388 | Self::Send(e) => write!(f, "{e}"), 389 | Self::Api(e) => write!(f, "{e}"), 390 | } 391 | } 392 | } 393 | 394 | impl From for CreateAccountsError { 395 | fn from(value: SendError) -> Self { 396 | Self::Send(value) 397 | } 398 | } 399 | 400 | impl From for CreateAccountsError { 401 | fn from(value: CreateAccountsApiError) -> Self { 402 | Self::Api(value) 403 | } 404 | } 405 | 406 | #[derive(Clone, Copy)] 407 | pub struct CreateTransferError(pub(crate) NonZeroU32); 408 | 409 | impl CreateTransferError { 410 | const CODE_RANGE: std::ops::RangeInclusive = 411 | sys_safe::MIN_CREATE_TRANSFER_ERROR_CODE..=sys_safe::MAX_CREATE_TRANSFER_ERROR_CODE; 412 | 413 | pub fn kind(self) -> CreateTransferErrorKind { 414 | let code = self.0.get(); 415 | if Self::CODE_RANGE.contains(&code) 416 | && !sys_safe::EXCLUDED_CREATE_TRANSFER_ERROR_CODES.contains(&code) 417 | { 418 | // SAFETY: We checked if it's in range right above. 419 | unsafe { mem::transmute::(code) } 420 | } else { 421 | CreateTransferErrorKind::UnstableUncategorized 422 | } 423 | } 424 | 425 | pub fn code(self) -> NonZeroU32 { 426 | self.0 427 | } 428 | } 429 | 430 | impl fmt::Debug for CreateTransferError { 431 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 432 | let mut d = f.debug_tuple("CreateTransferError"); 433 | let kind = self.kind(); 434 | if matches!(kind, CreateTransferErrorKind::UnstableUncategorized) { 435 | let code = self.code().get(); 436 | d.field(&code); 437 | } else { 438 | d.field(&kind); 439 | } 440 | d.finish() 441 | } 442 | } 443 | 444 | impl fmt::Display for CreateTransferError { 445 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 446 | write!(f, "{:?}", self.kind()) 447 | } 448 | } 449 | 450 | impl Error for CreateTransferError {} 451 | 452 | impl From for CreateTransferError { 453 | /// Constructs a [`CreateTransferError`] out of the provided [`CreateTransferErrorKind`]. 454 | /// 455 | /// # Panics 456 | /// 457 | /// Panics on the hidden [`CreateTransferErrorKind::UnstableUncategorized`] variant. 458 | fn from(value: CreateTransferErrorKind) -> Self { 459 | let this = Self(NonZeroU32::new(value as _).unwrap()); 460 | if matches!(this.kind(), CreateTransferErrorKind::UnstableUncategorized) { 461 | panic!("CreateTransferErrorKind::{value:?}") 462 | } 463 | this 464 | } 465 | } 466 | 467 | /// Type indicating individual API error for transfer creation. 468 | /// 469 | /// Safe to `transpose` from [`RawCreateTransfersIndividualApiResult`] 470 | /// if [`Self::from_raw_result_unchecked`] would also be safe. 471 | // INVARIANT: `self.0.result` must not be zero. 472 | #[derive(Clone, Copy)] 473 | #[repr(transparent)] 474 | pub struct CreateTransfersIndividualApiError(RawCreateTransfersIndividualApiResult); 475 | 476 | impl CreateTransfersIndividualApiError { 477 | /// Create error from raw struct. 478 | /// 479 | /// # Errors 480 | /// 481 | /// Returns `None` if `raw.result` is zero. 482 | pub fn from_raw_result(raw: RawCreateTransfersIndividualApiResult) -> Option { 483 | (raw.result != 0).then_some(Self(raw)) 484 | } 485 | 486 | /// Create error from raw struct. Unchecked version of [`Self::from_raw_result`]. 487 | /// 488 | /// # Safety 489 | /// 490 | /// This function is unsafe. `raw.result` must not be zero. 491 | pub unsafe fn from_raw_result_unchecked(raw: RawCreateTransfersIndividualApiResult) -> Self { 492 | Self(raw) 493 | } 494 | 495 | /// Create vec of errors from vec of raw results. 496 | /// 497 | /// Retains only elements `r` of vec `v` that satisfy `r.result != 0`. 498 | pub fn vec_from_raw_results(mut v: Vec) -> Vec { 499 | v.retain(|r| r.result != 0); 500 | unsafe { Self::vec_from_raw_results_unchecked(v) } 501 | } 502 | 503 | /// Create vec of errors from vec of raw results. Unchecked version of 504 | /// [`Self::vec_from_raw_results`] 505 | /// 506 | /// # Safety 507 | /// 508 | /// This function is unsafe. Every element `r` of vec `v` must satisfy 509 | /// `r.result != 0`. 510 | pub unsafe fn vec_from_raw_results_unchecked( 511 | v: Vec, 512 | ) -> Vec { 513 | let mut v = mem::ManuallyDrop::new(v); 514 | let len = v.len(); 515 | let cap = v.capacity(); 516 | let ptr = v.as_mut_ptr().cast::(); 517 | // SAFETY: this is fine because `Vec::from_raw_parts` has pretty loose 518 | // safety requirements, and since `CreateTransfersIndividualApiError` is 519 | // just a transparent wrapper of `RawCreateTransfersIndividualApiResult` 520 | // this is safe. 521 | Vec::from_raw_parts(ptr, len, cap) 522 | } 523 | 524 | /// Get index of the failed transfer. 525 | pub fn index(&self) -> u32 { 526 | self.0.index 527 | } 528 | 529 | /// Get error stripped of context, like index. 530 | pub fn inner(&self) -> CreateTransferError { 531 | CreateTransferError( 532 | // SAFETY: type invariant 533 | unsafe { NonZeroU32::new_unchecked(self.0.result) }, 534 | ) 535 | } 536 | 537 | /// Get kind of error to match upon. 538 | pub fn kind(&self) -> CreateTransferErrorKind { 539 | self.inner().kind() 540 | } 541 | } 542 | 543 | impl fmt::Debug for CreateTransfersIndividualApiError { 544 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 545 | f.debug_struct("CreateTransfersIndividualApiError") 546 | .field("index", &self.index()) 547 | .field("inner", &self.inner()) 548 | .finish() 549 | } 550 | } 551 | 552 | impl fmt::Display for CreateTransfersIndividualApiError { 553 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 554 | write!( 555 | f, 556 | "`{}` error occurred at account with index {}", 557 | self.inner(), 558 | self.index(), 559 | ) 560 | } 561 | } 562 | 563 | impl Error for CreateTransfersIndividualApiError {} 564 | 565 | // INVARIANT: `self.0` must not be empty. 566 | #[derive(Debug)] 567 | pub struct CreateTransfersApiError(Vec); 568 | 569 | impl CreateTransfersApiError { 570 | /// Get a slice of individual errors. Never empty. 571 | pub fn as_slice(&self) -> &[CreateTransfersIndividualApiError] { 572 | &self.0 573 | } 574 | 575 | /// Create error from vec of raw results. 576 | /// 577 | /// # Errors 578 | /// 579 | /// Returns `None` if `v.is_empty()`. 580 | pub fn from_errors(v: Vec) -> Option { 581 | (!v.is_empty()).then_some(CreateTransfersApiError(v)) 582 | } 583 | 584 | /// Create error from vec of raw results. 585 | /// 586 | /// Retains only results with errors. 587 | pub fn from_raw_results(v: Vec) -> Option { 588 | Self::from_errors(CreateTransfersIndividualApiError::vec_from_raw_results(v)) 589 | } 590 | } 591 | 592 | impl AsRef<[CreateTransfersIndividualApiError]> for CreateTransfersApiError { 593 | fn as_ref(&self) -> &[CreateTransfersIndividualApiError] { 594 | &self.0 595 | } 596 | } 597 | 598 | impl fmt::Display for CreateTransfersApiError { 599 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 600 | write!( 601 | f, 602 | "{} api errors occurred at transfers' creation", 603 | self.0.len(), 604 | ) 605 | } 606 | } 607 | 608 | impl Error for CreateTransfersApiError { 609 | fn source(&self) -> Option<&(dyn Error + 'static)> { 610 | self.0.first().map(|e| e as _) 611 | } 612 | } 613 | 614 | impl From for CreateTransfersApiError { 615 | fn from(value: CreateTransfersIndividualApiError) -> Self { 616 | Self(vec![value]) 617 | } 618 | } 619 | 620 | #[derive(Debug)] 621 | #[non_exhaustive] 622 | pub enum CreateTransfersError { 623 | Send(SendError), 624 | Api(CreateTransfersApiError), 625 | } 626 | 627 | impl Error for CreateTransfersError { 628 | fn source(&self) -> Option<&(dyn Error + 'static)> { 629 | Some(match self { 630 | Self::Send(e) => e as _, 631 | Self::Api(e) => e as _, 632 | }) 633 | } 634 | } 635 | 636 | impl fmt::Display for CreateTransfersError { 637 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 638 | write!(f, "Failed to create transfers: ")?; 639 | match self { 640 | Self::Send(e) => write!(f, "{e}"), 641 | Self::Api(e) => write!(f, "{e}"), 642 | } 643 | } 644 | } 645 | 646 | impl From for CreateTransfersError { 647 | fn from(value: SendError) -> Self { 648 | Self::Send(value) 649 | } 650 | } 651 | 652 | impl From for CreateTransfersError { 653 | fn from(value: CreateTransfersApiError) -> Self { 654 | Self::Api(value) 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | env, 4 | ffi::{OsStr, OsString}, 5 | fs::{self, File}, 6 | io::{self, Write}, 7 | iter, 8 | path::{self, Path, PathBuf}, 9 | process::Command, 10 | }; 11 | 12 | use quote::{quote, ToTokens as _}; 13 | use syn::{parse_quote, visit::Visit, visit_mut::VisitMut}; 14 | 15 | /// Version of the used [TigerBeetle] release. 16 | /// 17 | /// [TigerBeetle]: https://github.com/tigerbeetle/tigerbeetle 18 | const TIGERBEETLE_RELEASE: &str = "0.16.67"; 19 | 20 | /// Commit hash of the [`TIGERBEETLE_RELEASE`]. 21 | const TIGERBEETLE_COMMIT: &str = "2398229d5c1a83ae7091fe0f696207178de6d041"; 22 | 23 | fn target_to_lib_dir(target: &str) -> Option<&'static str> { 24 | match target { 25 | "aarch64-unknown-linux-gnu" => Some("aarch64-linux-gnu.2.27"), 26 | "aarch64-unknown-linux-musl" => Some("aarch64-linux-musl"), 27 | "aarch64-apple-darwin" => Some("aarch64-macos"), 28 | "x86_64-unknown-linux-gnu" => Some("x86_64-linux-gnu.2.27"), 29 | "x86_64-unknown-linux-musl" => Some("x86_64-linux-musl"), 30 | "x86_64-apple-darwin" => Some("x86_64-macos"), 31 | "x86_64-pc-windows-gnu" => Some("x86_64-windows"), 32 | _ => None, 33 | } 34 | } 35 | 36 | fn target_to_tigerbeetle_target(target: &str) -> Option<&'static str> { 37 | match target { 38 | "aarch64-unknown-linux-gnu" => Some("aarch64-linux"), 39 | "aarch64-unknown-linux-musl" => Some("aarch64-linux-musl"), 40 | "aarch64-apple-darwin" => Some("aarch64-macos"), 41 | "x86_64-unknown-linux-gnu" => Some("x86_64-linux"), 42 | "x86_64-unknown-linux-musl" => Some("x86_64-linux-musl"), 43 | "x86_64-apple-darwin" => Some("x86_64-macos"), 44 | "x86_64-pc-windows-gnu" => Some("x86_64-windows"), 45 | _ => None, 46 | } 47 | } 48 | 49 | fn main() { 50 | assert!(env!("CARGO_PKG_VERSION").ends_with(TIGERBEETLE_RELEASE)); 51 | let out_dir: PathBuf = env::var("OUT_DIR").unwrap().into(); 52 | let debug: bool = env::var("TB_CLIENT_DEBUG").map_or_else( 53 | |_| env::var("DEBUG").unwrap().parse().unwrap(), 54 | |s| s.parse().unwrap(), 55 | ); 56 | let target = env::var("TARGET").unwrap(); 57 | 58 | println!("cargo:rerun-if-env-changed=DOCS_RS"); 59 | println!("cargo:rerun-if-env-changed=TB_CLIENT_DEBUG"); 60 | println!("cargo:rerun-if-changed=src/wrapper.h"); 61 | 62 | let wrapper; 63 | if env::var("DOCS_RS").is_ok() { 64 | wrapper = "src/wrapper.h".into(); 65 | } else { 66 | let target_lib_subdir = target_to_lib_dir(&target) 67 | .unwrap_or_else(|| panic!("target `{target:?}` is not supported")); 68 | 69 | let tigerbeetle_target = target_to_tigerbeetle_target(&target) 70 | .unwrap_or_else(|| panic!("target `{target:?}` is not supported")); 71 | 72 | let tigerbeetle_root = out_dir.join("tigerbeetle"); 73 | fs::remove_dir_all(&tigerbeetle_root) 74 | .or_else(|e| { 75 | if let io::ErrorKind::NotFound = e.kind() { 76 | Ok(()) 77 | } else { 78 | Err(e) 79 | } 80 | }) 81 | .unwrap(); 82 | create_mirror( 83 | "tigerbeetle".as_ref(), 84 | &tigerbeetle_root, 85 | &["src/clients/c/lib", "zig-cache", "zig-out", ".git"] 86 | .into_iter() 87 | .collect(), 88 | ); 89 | 90 | let status = if cfg!(windows) { 91 | let mut cmd = Command::new("pwsh"); 92 | _ = cmd.arg(tigerbeetle_root.join("zig/download.win.ps1")); 93 | cmd 94 | } else { 95 | Command::new(tigerbeetle_root.join("zig/download.sh")) 96 | } 97 | .current_dir(&tigerbeetle_root) 98 | .status() 99 | .expect("running `download` script"); 100 | assert!(status.success(), "`download` script failed with {status:?}"); 101 | 102 | let status = Command::new( 103 | tigerbeetle_root 104 | .join("zig/zig") 105 | .with_extension(env::consts::EXE_EXTENSION) 106 | .canonicalize() 107 | .unwrap(), 108 | ) 109 | .arg("build") 110 | .arg("clients:c") 111 | .args((!debug).then_some("-Drelease")) 112 | .arg(format!("-Dtarget={tigerbeetle_target}")) 113 | .arg(format!("-Dconfig-release={TIGERBEETLE_RELEASE}")) 114 | .arg(format!("-Dconfig-release-client-min={TIGERBEETLE_RELEASE}")) 115 | .arg(format!("-Dgit-commit={TIGERBEETLE_COMMIT}")) 116 | .current_dir(&tigerbeetle_root) 117 | .env_remove("CI") 118 | .status() 119 | .expect("running `zig build` subcommand"); 120 | assert!(status.success(), "`zig build` failed with {status:?}"); 121 | 122 | let c_dir = tigerbeetle_root.join("src/clients/c/"); 123 | let lib_dir = tigerbeetle_root.join("src/clients/c/lib"); 124 | let link_search = lib_dir.join(target_lib_subdir); 125 | println!( 126 | "cargo:rustc-link-search=native={}", 127 | link_search 128 | .to_str() 129 | .expect("link search directory path is not valid unicode"), 130 | ); 131 | if target == "x86_64-pc-windows-gnu" { 132 | // `-gnu` toolchain looks for `lib.a` file of a static library by default, but 133 | // `zig build` produces `.lib` despite using MinGW under-the-hood. 134 | println!("cargo:rustc-link-lib=static:+verbatim=tb_client.lib"); 135 | // As of Rust 1.87, its `std` doesn't link `advapi32` automatically anymore, however 136 | // the `tb_client` requires it. 137 | // See: https://github.com/rust-lang/rust/pull/138233 138 | // https://github.com/rust-lang/rust/issues/139352 139 | println!("cargo:rustc-link-lib=advapi32"); 140 | } else { 141 | println!("cargo:rustc-link-lib=static=tb_client"); 142 | } 143 | 144 | wrapper = c_dir.join("wrapper.h"); 145 | let generated_header = c_dir.join("tb_client.h"); 146 | assert_eq!( 147 | fs::read_to_string(&generated_header).expect("reading generated `tb_client.h`"), 148 | fs::read_to_string("src/tb_client.h") 149 | .expect("reading pre-generated `tb_client.h`") 150 | .replace("\r\n", "\n"), 151 | "generated and pre-generated `tb_client.h` headers must be equal, \ 152 | generated at: {generated_header:?}", 153 | ); 154 | fs::copy("src/wrapper.h", &wrapper).expect("copying `wrapper.h`"); 155 | }; 156 | 157 | let bindings = bindgen::Builder::default() 158 | .header( 159 | wrapper 160 | .to_str() 161 | .expect("`wrapper.h` out path is not valid unicode"), 162 | ) 163 | .default_enum_style(bindgen::EnumVariation::ModuleConsts) 164 | .parse_callbacks(Box::new(TigerbeetleCallbacks { 165 | inner: bindgen::CargoCallbacks::default(), 166 | out_dir: out_dir.clone(), 167 | })) 168 | .generate() 169 | .expect("generating `tb_client` bindings"); 170 | 171 | let mut bindings = syn::parse_file(&bindings.to_string()).unwrap(); 172 | LintSuppressionVisitor.visit_file_mut(&mut bindings); 173 | 174 | let bindings_path = out_dir.join("bindings.rs"); 175 | fs::write(&bindings_path, bindings.to_token_stream().to_string()) 176 | .expect("writing `tb_client` bindings"); 177 | rustfmt(bindings_path); 178 | 179 | if env::var("CARGO_FEATURE_GENERATED_SAFE").is_ok() { 180 | let mut visitor = TigerbeetleVisitor::default(); 181 | visitor.visit_file(&bindings); 182 | 183 | let generated_path = out_dir.join("generated.rs"); 184 | let mut f = io::BufWriter::new(File::create(&generated_path).unwrap()); 185 | write!(f, "{}", visitor.output).unwrap(); 186 | drop(f); 187 | 188 | rustfmt(generated_path); 189 | } 190 | } 191 | 192 | struct LintSuppressionVisitor; 193 | 194 | impl VisitMut for LintSuppressionVisitor { 195 | fn visit_foreign_item_fn_mut(&mut self, i: &mut syn::ForeignItemFn) { 196 | // As of MSRV 1.78, `u128` is FFI-compatible: 197 | // https://blog.rust-lang.org/2024/03/30/i128-layout-update 198 | if i.sig.ident == "tb_client_init_parameters" { 199 | i.attrs.push(parse_quote! { 200 | #[allow(improper_ctypes)] 201 | }); 202 | } 203 | 204 | syn::visit_mut::visit_foreign_item_fn_mut(self, i) 205 | } 206 | } 207 | 208 | #[derive(Default)] 209 | struct TigerbeetleVisitor { 210 | output: proc_macro2::TokenStream, 211 | } 212 | 213 | impl Visit<'_> for TigerbeetleVisitor { 214 | fn visit_item_mod(&mut self, i: &syn::ItemMod) { 215 | let enum_ident = i.ident.clone(); 216 | let enum_name = enum_ident.to_string(); 217 | let mut prefix_enum = enum_name.as_str(); 218 | 219 | 'process: { 220 | if !prefix_enum.starts_with("TB_") { 221 | break 'process; 222 | } 223 | 224 | let Some((_, content)) = &i.content else { 225 | break 'process; 226 | }; 227 | let mut type_exists = false; 228 | let mut variants = Vec::new(); 229 | assert!(content.len() > 1); 230 | for item in content { 231 | match item { 232 | syn::Item::Const(c) => { 233 | let syn::Expr::Lit(syn::ExprLit { 234 | lit: syn::Lit::Int(i), 235 | .. 236 | }) = &*c.expr 237 | else { 238 | break 'process; 239 | }; 240 | let i = i.base10_parse::().unwrap(); 241 | variants.push((c.ident.to_string(), c.ident.clone(), i)); 242 | } 243 | syn::Item::Type(t) if t.ident == "Type" && !type_exists => type_exists = true, 244 | _ => break 'process, 245 | } 246 | } 247 | 248 | 'remove_variant_prefix: { 249 | while !variants.iter().all(|(n, _, _)| n.starts_with(prefix_enum)) { 250 | match prefix_enum.rsplit_once('_') { 251 | Some((n, _)) => prefix_enum = n, 252 | None => break 'remove_variant_prefix, 253 | } 254 | } 255 | 256 | variants.iter_mut().for_each(|(n, _, _)| { 257 | *n = n 258 | .strip_prefix(prefix_enum) 259 | .and_then(|n| n.strip_prefix('_')) 260 | .unwrap() 261 | .into() 262 | }); 263 | } 264 | 265 | variants.sort_unstable_by_key(|(_, _, i)| *i); 266 | 267 | let mut new_enum_name = 268 | screaming_snake_case_into_camel_case(enum_name.strip_prefix("TB_").unwrap()); 269 | let mut new_enum_ident = syn::Ident::new(&new_enum_name, enum_ident.span()); 270 | 271 | if enum_name.ends_with("_FLAGS") { 272 | let ty = syn::Ident::new( 273 | match enum_name.as_str() { 274 | "TB_ACCOUNT_FILTER_FLAGS" | "TB_QUERY_FILTER_FLAGS" => "u32", 275 | "TB_ACCOUNT_FLAGS" | "TB_TRANSFER_FLAGS" => "u16", 276 | other => panic!("unexpected flags type name: {other}"), 277 | }, 278 | enum_ident.span(), 279 | ); 280 | let variants = variants.iter().map(|(n, v, _)| { 281 | let n = syn::Ident::new(n, v.span()); 282 | quote!(const #n = super:: #enum_ident :: #v as #ty;) 283 | }); 284 | self.output.extend(quote! { 285 | ::bitflags::bitflags! { 286 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 287 | pub struct #new_enum_ident: #ty { 288 | #(#variants)* 289 | } 290 | } 291 | }) 292 | } else { 293 | variants.iter_mut().for_each(|(n, _, _)| { 294 | *n = screaming_snake_case_into_camel_case(n); 295 | }); 296 | 297 | let mut errorize = false; 298 | let mut repr_type = "u32"; 299 | if let Some(n) = new_enum_name.strip_suffix("Result") { 300 | new_enum_name = format!("{n}ErrorKind"); 301 | new_enum_ident = syn::Ident::new(&new_enum_name, new_enum_ident.span()); 302 | errorize = true; 303 | } 304 | match new_enum_name.as_str() { 305 | "ClientStatus" 306 | | "InitStatus" 307 | | "PacketStatus" 308 | | "PacketAcquireStatus" 309 | | "RegisterLogCallbackStatus" => { 310 | if new_enum_name == "PacketStatus" { 311 | repr_type = "u8"; 312 | } 313 | new_enum_name = format!("{new_enum_name}ErrorKind"); 314 | new_enum_ident = syn::Ident::new(&new_enum_name, new_enum_ident.span()); 315 | errorize = true; 316 | } 317 | "Operation" => { 318 | repr_type = "u8"; 319 | new_enum_name = "OperationKind".into(); 320 | new_enum_ident = syn::Ident::new(&new_enum_name, new_enum_ident.span()); 321 | } 322 | _ => (), 323 | } 324 | 325 | let repr_type = syn::Ident::new(repr_type, proc_macro2::Span::call_site()); 326 | 327 | if errorize { 328 | let first_variant = variants.first().unwrap(); 329 | assert!( 330 | matches!(first_variant.0.as_str(), "Ok" | "Success"), 331 | "variant name is {:?}", 332 | first_variant.0, 333 | ); 334 | assert_eq!(first_variant.2, 0); 335 | variants.remove(0); 336 | } 337 | 338 | let all_codes = variants 339 | .iter() 340 | .filter_map(|(_, _, c)| (*c != 0).then_some(*c)) 341 | .collect::>(); 342 | let min_code = *all_codes.iter().min().unwrap(); 343 | let max_code = *all_codes.iter().max().unwrap(); 344 | let excluded_codes = (min_code..=max_code) 345 | .filter(|c| !all_codes.contains(c)) 346 | .collect::>(); 347 | 348 | let minmax_prefix = enum_name 349 | .strip_suffix("_RESULT") 350 | .unwrap_or(&enum_name) 351 | .strip_prefix("TB_") 352 | .unwrap(); 353 | let error = if errorize { "_ERROR" } else { "" }; 354 | 355 | let min_name = syn::Ident::new( 356 | &format!("MIN_{minmax_prefix}{error}_CODE"), 357 | proc_macro2::Span::call_site(), 358 | ); 359 | let max_name = syn::Ident::new( 360 | &format!("MAX_{minmax_prefix}{error}_CODE"), 361 | proc_macro2::Span::call_site(), 362 | ); 363 | let min_code = 364 | syn::LitInt::new(&min_code.to_string(), proc_macro2::Span::call_site()); 365 | let max_code = 366 | syn::LitInt::new(&max_code.to_string(), proc_macro2::Span::call_site()); 367 | let excluded_const = (!excluded_codes.is_empty()).then(|| { 368 | let excl_name = syn::Ident::new( 369 | &format!("EXCLUDED_{minmax_prefix}{error}_CODES"), 370 | proc_macro2::Span::call_site(), 371 | ); 372 | let excl_codes = excluded_codes 373 | .iter() 374 | .map(|c| syn::LitInt::new(&c.to_string(), proc_macro2::Span::call_site())); 375 | quote! { 376 | pub const #excl_name: &[#repr_type] = &[#(#excl_codes),*]; 377 | } 378 | }); 379 | let extra = quote! { 380 | pub const #min_name: #repr_type = #min_code; 381 | pub const #max_name: #repr_type = #max_code; 382 | #excluded_const 383 | }; 384 | 385 | let from_snake_case_str_branches = variants 386 | .iter() 387 | .map(|(s, v, _)| { 388 | let n = syn::Ident::new(s, v.span()); 389 | let s = camel_case_into_snake_case(s); 390 | quote!(#s => Some(Self:: #n)) 391 | }) 392 | .chain(iter::once(quote!( 393 | _ => None 394 | ))); 395 | 396 | let into_snake_case_str_branches = variants 397 | .iter() 398 | .map(|(s, v, _)| { 399 | let n = syn::Ident::new(s, v.span()); 400 | let s = camel_case_into_snake_case(s); 401 | quote!(Self:: #n => #s) 402 | }) 403 | .chain(iter::once(quote!( 404 | Self::UnstableUncategorized => unimplemented!("variant is not supported yet") 405 | ))); 406 | 407 | let variants = variants 408 | .iter() 409 | .map(|(n, v, _)| { 410 | let n = syn::Ident::new(n, v.span()); 411 | quote!(#n = super:: #enum_ident :: #v as #repr_type) 412 | }) 413 | .chain(iter::once(quote!( 414 | #[doc(hidden)] 415 | UnstableUncategorized 416 | ))); 417 | 418 | let first_doc_str_from_snake_case_str = 419 | format!("Try parsing [`{new_enum_name}`] from a string slice"); 420 | let first_doc_str_into_snake_case_str = format!( 421 | "Returns a static string slice according to [`{new_enum_name}`] variant's name but in snake_case" 422 | ); 423 | 424 | self.output.extend(quote! { 425 | #[derive(Debug, Clone, Copy)] 426 | #[non_exhaustive] 427 | #[repr( #repr_type )] 428 | pub enum #new_enum_ident { 429 | #(#variants),* 430 | } 431 | 432 | impl #new_enum_ident { 433 | #[doc = #first_doc_str_from_snake_case_str] 434 | #[doc = ""] 435 | #[doc = "# Stability"] 436 | #[doc = ""] 437 | #[doc = "Might return `Some` instead of `None` after a minor version bump"] 438 | pub fn from_snake_case_str(s: &str) -> Option { 439 | match s { 440 | #(#from_snake_case_str_branches),* 441 | } 442 | } 443 | 444 | #[doc = #first_doc_str_into_snake_case_str] 445 | pub fn into_snake_case_str(self) -> &'static str { 446 | match self { 447 | #(#into_snake_case_str_branches),* 448 | } 449 | } 450 | } 451 | }); 452 | self.output.extend(extra); 453 | } 454 | } 455 | 456 | syn::visit::visit_item_mod(self, i) 457 | } 458 | } 459 | 460 | #[derive(Debug)] 461 | struct TigerbeetleCallbacks { 462 | inner: bindgen::CargoCallbacks, 463 | out_dir: PathBuf, 464 | } 465 | 466 | impl bindgen::callbacks::ParseCallbacks for TigerbeetleCallbacks { 467 | fn add_derives(&self, info: &bindgen::callbacks::DeriveInfo<'_>) -> Vec { 468 | let mut out = Vec::new(); 469 | if let bindgen::callbacks::DeriveInfo { 470 | kind: bindgen::callbacks::TypeKind::Struct, 471 | name: 472 | "tb_account_t" 473 | | "tb_account_balance_t" 474 | | "tb_account_filter_t" 475 | | "tb_create_accounts_result_t" 476 | | "tb_create_transfers_result_t" 477 | | "tb_query_filter_t" 478 | | "tb_transfer_t", 479 | .. 480 | } = info 481 | { 482 | out.extend(["::bytemuck::Pod".into(), "::bytemuck::Zeroable".into()]); 483 | }; 484 | if let bindgen::callbacks::DeriveInfo { 485 | kind: bindgen::callbacks::TypeKind::Struct, 486 | name: "tb_init_parameters_t", 487 | .. 488 | } = info 489 | { 490 | out.extend(["::bytemuck::Zeroable".into()]); 491 | }; 492 | out.append(&mut self.inner.add_derives(info)); 493 | out 494 | } 495 | 496 | fn will_parse_macro(&self, name: &str) -> bindgen::callbacks::MacroParsingBehavior { 497 | self.inner.will_parse_macro(name) 498 | } 499 | 500 | fn generated_name_override( 501 | &self, 502 | item_info: bindgen::callbacks::ItemInfo<'_>, 503 | ) -> Option { 504 | self.inner.generated_name_override(item_info) 505 | } 506 | 507 | fn generated_link_name_override( 508 | &self, 509 | item_info: bindgen::callbacks::ItemInfo<'_>, 510 | ) -> Option { 511 | self.inner.generated_link_name_override(item_info) 512 | } 513 | 514 | fn int_macro(&self, name: &str, value: i64) -> Option { 515 | self.inner.int_macro(name, value) 516 | } 517 | 518 | fn str_macro(&self, name: &str, value: &[u8]) { 519 | self.inner.str_macro(name, value) 520 | } 521 | 522 | fn func_macro(&self, name: &str, value: &[&[u8]]) { 523 | self.inner.func_macro(name, value) 524 | } 525 | 526 | fn enum_variant_behavior( 527 | &self, 528 | enum_name: Option<&str>, 529 | original_variant_name: &str, 530 | variant_value: bindgen::callbacks::EnumVariantValue, 531 | ) -> Option { 532 | self.inner 533 | .enum_variant_behavior(enum_name, original_variant_name, variant_value) 534 | } 535 | 536 | fn enum_variant_name( 537 | &self, 538 | enum_name: Option<&str>, 539 | original_variant_name: &str, 540 | variant_value: bindgen::callbacks::EnumVariantValue, 541 | ) -> Option { 542 | self.inner 543 | .enum_variant_name(enum_name, original_variant_name, variant_value) 544 | } 545 | 546 | fn item_name(&self, original_item: bindgen::callbacks::ItemInfo<'_>) -> Option { 547 | self.inner.item_name(original_item) 548 | } 549 | 550 | fn include_file(&self, filename: &str) { 551 | if !Path::new(filename).starts_with(&self.out_dir) { 552 | self.inner.include_file(filename) 553 | } 554 | } 555 | 556 | fn read_env_var(&self, key: &str) { 557 | self.inner.read_env_var(key) 558 | } 559 | 560 | fn blocklisted_type_implements_trait( 561 | &self, 562 | name: &str, 563 | derive_trait: bindgen::callbacks::DeriveTrait, 564 | ) -> Option { 565 | self.inner 566 | .blocklisted_type_implements_trait(name, derive_trait) 567 | } 568 | 569 | fn process_comment(&self, comment: &str) -> Option { 570 | self.inner.process_comment(comment) 571 | } 572 | } 573 | 574 | fn screaming_snake_case_into_camel_case(src: &str) -> String { 575 | let mut dst = String::with_capacity(src.len()); 576 | for word in src.split('_') { 577 | let mut chars = word.chars(); 578 | let Some(ch) = chars.next() else { continue }; 579 | assert!(ch.is_ascii_uppercase() || ch.is_ascii_digit()); 580 | dst.push(ch); 581 | dst.extend(chars.map(|c| c.to_ascii_lowercase())); 582 | } 583 | dst 584 | } 585 | 586 | fn camel_case_into_snake_case(src: &str) -> String { 587 | let mut chars = src.chars(); 588 | let Some(ch) = chars.next() else { 589 | return String::new(); 590 | }; 591 | assert!(ch.is_ascii_uppercase()); 592 | 593 | let mut dst = String::with_capacity(src.len() * 2); 594 | dst.push(ch.to_ascii_lowercase()); 595 | 596 | dst.extend(chars.flat_map(|c| { 597 | if c.is_ascii_uppercase() { 598 | Some('_') 599 | .into_iter() 600 | .chain(iter::once(c.to_ascii_lowercase())) 601 | } else { 602 | None.into_iter().chain(iter::once(c)) 603 | } 604 | })); 605 | dst 606 | } 607 | 608 | fn create_mirror(original: &Path, mirror: &Path, ignores: &IgnoreNode) { 609 | if ignores.ignored() { 610 | return; 611 | } 612 | 613 | assert!(!mirror.exists(), "mirror path is occupied already"); 614 | let mirror_parent = mirror 615 | .parent() 616 | .expect("mirror should have parent directory"); 617 | assert!(mirror_parent.is_dir(), "mirror's parent is not a directory"); 618 | 619 | if ignores.inner_is_empty() { 620 | let original = original 621 | .canonicalize() 622 | .expect("Could not canonicalize original path"); 623 | 624 | let common_root = original 625 | .iter() 626 | .zip(mirror.iter()) 627 | .take_while(|(a, b)| a == b) 628 | .map(|(a, _)| a) 629 | .collect::(); 630 | 631 | let mirror_from_common = mirror.strip_prefix(&common_root).unwrap(); 632 | let original_from_common = original.strip_prefix(&common_root).unwrap(); 633 | let link_original: PathBuf = (0..mirror_from_common.iter().count() - 1) 634 | .map(|_| Path::new("..")) 635 | .chain(iter::once(original_from_common)) 636 | .collect(); 637 | 638 | return symlink(link_original, mirror).expect("Symlinking the mirror fragment"); 639 | } 640 | 641 | let original_traversal = original 642 | .read_dir() 643 | .expect("Initiating traversal of original directory"); 644 | std::fs::create_dir(mirror).expect("Creating mirror dir"); 645 | for entry in original_traversal { 646 | let entry = entry.expect("Reading original directory"); 647 | let entry_name = entry.file_name(); 648 | create_mirror( 649 | &original.join(&entry_name), 650 | &mirror.join(&entry_name), 651 | ignores.get(&entry_name), 652 | ); 653 | } 654 | } 655 | 656 | #[derive(Default)] 657 | struct IgnoreNode { 658 | inner: BTreeMap, 659 | ignored: bool, 660 | } 661 | 662 | impl IgnoreNode { 663 | const fn new() -> Self { 664 | IgnoreNode { 665 | inner: BTreeMap::new(), 666 | ignored: false, 667 | } 668 | } 669 | 670 | fn get(&self, path_component: &OsStr) -> &IgnoreNode { 671 | static EMPTY: IgnoreNode = IgnoreNode::new(); 672 | self.inner.get(path_component).unwrap_or(&EMPTY) 673 | } 674 | 675 | fn ignored(&self) -> bool { 676 | self.ignored 677 | } 678 | 679 | fn inner_is_empty(&self) -> bool { 680 | self.inner.is_empty() 681 | } 682 | 683 | fn insert(&mut self, path: &Path) { 684 | path.components().for_each(|c| { 685 | assert!( 686 | matches!(c, path::Component::Normal(_)), 687 | "path component {c:?} must be `Normal(_)` instead" 688 | ) 689 | }); 690 | 691 | fn impl_(node: &mut IgnoreNode, path: &Path) { 692 | let mut iter = path.iter(); 693 | let Some(component) = iter.next() else { 694 | panic!("path is empty") 695 | }; 696 | let v = node.inner.entry(component.to_owned()).or_default(); 697 | let path = iter.as_path(); 698 | if path == Path::new("") { 699 | v.ignored = true; 700 | return; 701 | } 702 | impl_(v, path) 703 | } 704 | 705 | impl_(self, path) 706 | } 707 | } 708 | 709 | impl> Extend for IgnoreNode { 710 | fn extend>(&mut self, iter: T) { 711 | for path in iter { 712 | self.insert(path.as_ref()) 713 | } 714 | } 715 | } 716 | 717 | impl> FromIterator for IgnoreNode { 718 | fn from_iter>(iter: T) -> Self { 719 | let mut out = Self::new(); 720 | out.extend(iter); 721 | out 722 | } 723 | } 724 | 725 | fn symlink(original: P, link: Q) -> io::Result<()> 726 | where 727 | P: AsRef, 728 | Q: AsRef, 729 | { 730 | #[cfg(unix)] 731 | return std::os::unix::fs::symlink(original, link); 732 | #[cfg(windows)] 733 | return { 734 | let meta = link 735 | .as_ref() 736 | .parent() 737 | .ok_or(io::ErrorKind::NotFound)? 738 | .join(original.as_ref()) 739 | .metadata()?; 740 | if meta.is_file() { 741 | std::os::windows::fs::symlink_file(original, link) 742 | } else if meta.is_dir() { 743 | std::os::windows::fs::symlink_dir(original, link) 744 | } else { 745 | Err(io::ErrorKind::NotFound.into()) 746 | } 747 | }; 748 | #[cfg(not(any(unix, windows)))] 749 | unimplemented!("symlink on current platform is not supported") 750 | } 751 | 752 | /// Runs `rustfmt` over the provided `file`. 753 | /// 754 | /// # Panics 755 | /// 756 | /// If `rustfmt` fails to run. 757 | fn rustfmt(file: impl AsRef) { 758 | let file = file.as_ref(); 759 | 760 | Command::new(env::var("RUSTFMT").unwrap_or_else(|_| "rustfmt".into())) 761 | .arg(file) 762 | .status() 763 | .unwrap_or_else(|e| panic!("failed to run `rustfmt` on {}: {e}", file.display())); 764 | } 765 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | `tigerbeetle-unofficial` changelog 2 | ================================== 3 | 4 | All user visible changes to `tigerbeetle-unofficial`, `tigerbeetle-unofficial-core` and `tigerbeetle-unofficial-sys` crates will be documented in this file. This project uses [Semantic Versioning 2.0.0]. 5 | 6 | 7 | 8 | 9 | ## [0.14.17+0.16.67] · 2025-12-16 10 | [0.14.17+0.16.67]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.17%2B0.16.67 11 | 12 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.16%2B0.16.66...v0.14.17%2B0.16.67) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/51) 13 | 14 | ### Changed 15 | 16 | - Upgraded [`tb_client` C library] to [0.16.67 version][tb-0.16.67]. ([#99]) 17 | 18 | [#99]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/99 19 | [tb-0.16.67]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.67/CHANGELOG.md#tigerbeetle-01667 20 | 21 | 22 | 23 | 24 | ## [0.14.16+0.16.66] · 2025-11-25 25 | [0.14.16+0.16.66]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.16%2B0.16.66 26 | 27 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.15%2B0.16.65...v0.14.16%2B0.16.66) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/50) 28 | 29 | ### Changed 30 | 31 | - Upgraded [`tb_client` C library] to [0.16.66 version][tb-0.16.66]. ([#97]) 32 | 33 | [#97]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/97 34 | [tb-0.16.66]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.66/CHANGELOG.md#tigerbeetle-01666 35 | 36 | 37 | 38 | 39 | ## [0.14.15+0.16.65] · 2025-11-18 40 | [0.14.15+0.16.65]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.15%2B0.16.65 41 | 42 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.14%2B0.16.64...v0.14.15%2B0.16.65) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/49) 43 | 44 | ### Changed 45 | 46 | - Upgraded [`tb_client` C library] to [0.16.65 version][tb-0.16.65]. ([#95]) 47 | 48 | [#95]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/95 49 | [tb-0.16.65]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.65/CHANGELOG.md#tigerbeetle-01665 50 | 51 | 52 | 53 | 54 | ## [0.14.14+0.16.64] · 2025-11-11 55 | [0.14.14+0.16.64]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.14%2B0.16.64 56 | 57 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.13%2B0.16.63...v0.14.14%2B0.16.64) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/48) 58 | 59 | ### Changed 60 | 61 | - Upgraded [`tb_client` C library] to [0.16.64 version][tb-0.16.64]. ([#94]) 62 | 63 | [#94]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/94 64 | [tb-0.16.64]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.64/CHANGELOG.md#tigerbeetle-01664 65 | 66 | 67 | 68 | 69 | ## [0.14.13+0.16.63] · 2025-11-04 70 | [0.14.13+0.16.63]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.13%2B0.16.63 71 | 72 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.12%2B0.16.62...v0.14.13%2B0.16.63) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/47) 73 | 74 | ### Changed 75 | 76 | - Upgraded [`tb_client` C library] to [0.16.63 version][tb-0.16.63]. ([#93]) 77 | 78 | [#93]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/93 79 | [tb-0.16.63]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.63/CHANGELOG.md#tigerbeetle-01663 80 | 81 | 82 | 83 | 84 | ## [0.14.12+0.16.62] · 2025-10-21 85 | [0.14.12+0.16.62]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.12%2B0.16.62 86 | 87 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.11%2B0.16.61...v0.14.12%2B0.16.62) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/46) 88 | 89 | ### Changed 90 | 91 | - Upgraded [`tb_client` C library] to [0.16.62 version][tb-0.16.62]. ([#92]) 92 | 93 | [#92]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/92 94 | [tb-0.16.62]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.62/CHANGELOG.md#tigerbeetle-01662 95 | 96 | 97 | 98 | 99 | ## [0.14.11+0.16.61] · 2025-10-07 100 | [0.14.11+0.16.61]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.11%2B0.16.61 101 | 102 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.10%2B0.16.60...v0.14.11%2B0.16.61) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/45) 103 | 104 | ### Changed 105 | 106 | - Upgraded [`tb_client` C library] to [0.16.61 version][tb-0.16.61]. ([#91]) 107 | 108 | [#91]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/91 109 | [tb-0.16.61]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.61/CHANGELOG.md#tigerbeetle-01661 110 | 111 | 112 | 113 | 114 | ## [0.14.10+0.16.60] · 2025-09-30 115 | [0.14.10+0.16.60]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.10%2B0.16.60 116 | 117 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.9%2B0.16.59...v0.14.10%2B0.16.60) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/44) 118 | 119 | ### Changed 120 | 121 | - Upgraded [`tb_client` C library] to [0.16.60 version][tb-0.16.60]. ([#90]) 122 | 123 | [#90]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/90 124 | [tb-0.16.60]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.60/CHANGELOG.md#tigerbeetle-01660 125 | 126 | 127 | 128 | 129 | ## [0.14.9+0.16.59] · 2025-09-23 130 | [0.14.9+0.16.59]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.9%2B0.16.59 131 | 132 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.8%2B0.16.58...v0.14.9%2B0.16.59) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/43) 133 | 134 | ### Changed 135 | 136 | - Upgraded [`tb_client` C library] to [0.16.59 version][tb-0.16.59]. ([#89]) 137 | 138 | [#89]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/89 139 | [tb-0.16.59]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.59/CHANGELOG.md#tigerbeetle-01659 140 | 141 | 142 | 143 | 144 | ## [0.14.8+0.16.58] · 2025-09-16 145 | [0.14.8+0.16.58]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.8%2B0.16.58 146 | 147 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.7%2B0.16.57...v0.14.8%2B0.16.58) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/42) 148 | 149 | ### Changed 150 | 151 | - Upgraded [`tb_client` C library] to [0.16.58 version][tb-0.16.58]. ([#88]) 152 | 153 | [#88]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/88 154 | [tb-0.16.58]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.58/CHANGELOG.md#tigerbeetle-01658 155 | 156 | 157 | 158 | 159 | ## [0.14.7+0.16.57] · 2025-09-02 160 | [0.14.7+0.16.57]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.7%2B0.16.57 161 | 162 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.6%2B0.16.56...v0.14.7%2B0.16.57) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/41) 163 | 164 | ### Changed 165 | 166 | - Upgraded [`tb_client` C library] to [0.16.57 version][tb-0.16.57]. ([#87]) 167 | 168 | [#87]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/87 169 | [tb-0.16.57]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.57/CHANGELOG.md#tigerbeetle-01657 170 | 171 | 172 | 173 | 174 | ## [0.14.6+0.16.56] · 2025-08-26 175 | [0.14.6+0.16.56]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.6%2B0.16.56 176 | 177 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.5%2B0.16.55...v0.14.6%2B0.16.56) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/40) 178 | 179 | ### Changed 180 | 181 | - Upgraded [`tb_client` C library] to [0.16.56 version][tb-0.16.56]. ([#86]) 182 | 183 | [#86]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/86 184 | [tb-0.16.56]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.56/CHANGELOG.md#tigerbeetle-01656 185 | 186 | 187 | 188 | 189 | ## [0.14.5+0.16.55] · 2025-08-19 190 | [0.14.5+0.16.55]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.5%2B0.16.55 191 | 192 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.4%2B0.16.54...v0.14.5%2B0.16.55) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/39) 193 | 194 | ### Changed 195 | 196 | - Upgraded [`tb_client` C library] to [0.16.55 version][tb-0.16.55]. ([#85]) 197 | 198 | [#85]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/85 199 | [tb-0.16.55]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.55/CHANGELOG.md#tigerbeetle-01655 200 | 201 | 202 | 203 | 204 | ## [0.14.4+0.16.54] · 2025-08-12 205 | [0.14.4+0.16.54]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.4%2B0.16.54 206 | 207 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.3%2B0.16.53...v0.14.4%2B0.16.54) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/38) 208 | 209 | ### Changed 210 | 211 | - Upgraded [`tb_client` C library] to [0.16.54 version][tb-0.16.54]. ([#84]) 212 | 213 | [#84]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/84 214 | [tb-0.16.54]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.54/CHANGELOG.md#tigerbeetle-01654 215 | 216 | 217 | 218 | 219 | ## [0.14.3+0.16.53] · 2025-08-05 220 | [0.14.3+0.16.53]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.3%2B0.16.53 221 | 222 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.2%2B0.16.52...v0.14.3%2B0.16.53) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/37) 223 | 224 | ### Changed 225 | 226 | - Upgraded [`tb_client` C library] to [0.16.53 version][tb-0.16.53]. ([#82]) 227 | 228 | [#82]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/82 229 | [tb-0.16.53]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.53/CHANGELOG.md#tigerbeetle-01653 230 | 231 | 232 | 233 | 234 | ## [0.14.2+0.16.52] · 2025-07-29 235 | [0.14.2+0.16.52]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.2%2B0.16.52 236 | 237 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.1%2B0.16.51...v0.14.2%2B0.16.52) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/36) 238 | 239 | ### Changed 240 | 241 | - Upgraded [`tb_client` C library] to [0.16.52 version][tb-0.16.52]. ([#81]) 242 | 243 | [#81]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/81 244 | [tb-0.16.52]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.52/CHANGELOG.md#tigerbeetle-01652 245 | 246 | 247 | 248 | 249 | ## [0.14.1+0.16.51] · 2025-07-22 250 | [0.14.1+0.16.51]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.1%2B0.16.51 251 | 252 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.14.0%2B0.16.50...v0.14.1%2B0.16.51) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/35) 253 | 254 | ### Changed 255 | 256 | - Upgraded [`tb_client` C library] to [0.16.51 version][tb-0.16.51]. ([#80]) 257 | 258 | [#80]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/80 259 | [tb-0.16.51]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.51/CHANGELOG.md#tigerbeetle-01651 260 | 261 | 262 | 263 | 264 | ## [0.14.0+0.16.50] · 2025-07-16 265 | [0.14.0+0.16.50]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.14.0%2B0.16.50 266 | 267 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.13.2%2B0.16.49...v0.14.0%2B0.16.50) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/34) 268 | 269 | ### Fixed 270 | 271 | - `core` crate: 272 | - `Callbacks` panic on `.submit()` `Future` cancellation. ([#78]) 273 | 274 | ### Changed 275 | 276 | - Upgraded [`tb_client` C library] to [0.16.50 version][tb-0.16.50]. ([#79]) 277 | 278 | [#78]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/78 279 | [#79]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/79 280 | [tb-0.16.50]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.50/CHANGELOG.md#tigerbeetle-01650 281 | 282 | 283 | 284 | 285 | ## [0.13.2+0.16.49] · 2025-07-08 286 | [0.13.2+0.16.49]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.13.2%2B0.16.49 287 | 288 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.13.1%2B0.16.48...v0.13.2%2B0.16.49) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/33) 289 | 290 | ### Changed 291 | 292 | - Upgraded [`tb_client` C library] to [0.16.49 version][tb-0.16.49]. ([#77]) 293 | 294 | [#77]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/77 295 | [tb-0.16.49]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.49/CHANGELOG.md#tigerbeetle-01649 296 | 297 | 298 | 299 | 300 | ## [0.13.1+0.16.48] · 2025-07-02 301 | [0.13.1+0.16.48]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.13.1%2B0.16.48 302 | 303 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.13.0%2B0.16.47...v0.13.1%2B0.16.48) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/32) 304 | 305 | ### Changed 306 | 307 | - Upgraded [`tb_client` C library] to [0.16.48 version][tb-0.16.48]. ([#76]) 308 | 309 | [#76]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/76 310 | [tb-0.16.48]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.48/CHANGELOG.md#tigerbeetle-01648 311 | 312 | 313 | 314 | 315 | ## [0.13.0+0.16.47] · 2025-07-01 316 | [0.13.0+0.16.47]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.13.0%2B0.16.47 317 | 318 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.12.3%2B0.16.46...v0.13.0%2B0.16.47) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/31) 319 | 320 | ### BC Breaks 321 | 322 | - `sys` crate: 323 | - Changed size of `opaque` field in `tb_packet_t` struct from 32 to 64 bytes. ([#75], [tigerbeetle/tigerbeetle#3038]) 324 | 325 | ### Changed 326 | 327 | - Upgraded [`tb_client` C library] to [0.16.47 version][tb-0.16.47]. ([#75]) 328 | 329 | [#75]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/75 330 | [tb-0.16.47]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.47/CHANGELOG.md#tigerbeetle-01647 331 | [tigerbeetle/tigerbeetle#3038]: https://github.com/tigerbeetle/tigerbeetle/pull/3038 332 | 333 | 334 | 335 | 336 | ## [0.12.3+0.16.46] · 2025-06-24 337 | [0.12.3+0.16.46]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.12.3%2B0.16.46 338 | 339 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.12.2%2B0.16.45...v0.12.3%2B0.16.46) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/30) 340 | 341 | ### Changed 342 | 343 | - Upgraded [`tb_client` C library] to [0.16.46 version][tb-0.16.46]. ([#74]) 344 | 345 | [#74]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/74 346 | [tb-0.16.46]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.46/CHANGELOG.md#tigerbeetle-01646 347 | 348 | 349 | 350 | 351 | ## [0.12.2+0.16.45] · 2025-06-17 352 | [0.12.2+0.16.45]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.12.2%2B0.16.45 353 | 354 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.12.1%2B0.16.44...v0.12.2%2B0.16.45) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/29) 355 | 356 | ### Changed 357 | 358 | - Upgraded [`tb_client` C library] to [0.16.45 version][tb-0.16.45]. ([#73]) 359 | 360 | [#73]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/73 361 | [tb-0.16.45]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.45/CHANGELOG.md#tigerbeetle-01645 362 | 363 | 364 | 365 | 366 | ## [0.12.1+0.16.44] · 2025-06-10 367 | [0.12.1+0.16.44]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.12.1%2B0.16.44 368 | 369 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.12.0%2B0.16.43...v0.12.1%2B0.16.44) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/28) 370 | 371 | ### Changed 372 | 373 | - Upgraded [`tb_client` C library] to [0.16.44 version][tb-0.16.44]. ([#72]) 374 | 375 | [#72]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/72 376 | [tb-0.16.44]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.44/CHANGELOG.md#tigerbeetle-01644 377 | 378 | 379 | 380 | 381 | ## [0.12.0+0.16.43] · 2025-06-03 382 | [0.12.0+0.16.43]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.12.0%2B0.16.43 383 | 384 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.8%2B0.16.42...v0.12.0%2B0.16.43) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/27) 385 | 386 | ### BC Breaks 387 | 388 | - `sys` crate: 389 | - Renamed `TB_OPERATION_GET_EVENTS` value to `TB_OPERATION_GET_CHANGE_EVENTS` in `TB_OPERATION` enumeration. ([#70], [tigerbeetle/tigerbeetle#2917]) 390 | - `generated_safe`: 391 | - Renamed `OperationKind::GetEvents` enum variant to `OperationKind::GetChangeEvents`. ([#70], [tigerbeetle/tigerbeetle#2917]) 392 | - `core` crate: 393 | - Renamed `OperationKind::GetEvents` enum variant to `OperationKind::GetChangeEvents`. ([#70], [tigerbeetle/tigerbeetle#2917]) 394 | 395 | ### Changed 396 | 397 | - Upgraded [`tb_client` C library] to [0.16.43 version][tb-0.16.43]. ([#70]) 398 | 399 | [#70]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/70 400 | [tb-0.16.43]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.43/CHANGELOG.md#tigerbeetle-01643 401 | [tigerbeetle/tigerbeetle#2917]: https://github.com/tigerbeetle/tigerbeetle/pull/2917 402 | 403 | 404 | 405 | 406 | ## [0.11.8+0.16.42] · 2025-05-27 407 | [0.11.8+0.16.42]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.8%2B0.16.42 408 | 409 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.7%2B0.16.41...v0.11.8%2B0.16.42) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/26) 410 | 411 | ### Changed 412 | 413 | - Upgraded [`tb_client` C library] to [0.16.42 version][tb-0.16.42]. ([#69]) 414 | 415 | [#69]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/69 416 | [tb-0.16.42]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.42/CHANGELOG.md#tigerbeetle-01642 417 | 418 | 419 | 420 | 421 | ## [0.11.7+0.16.41] · 2025-05-20 422 | [0.11.7+0.16.41]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.7%2B0.16.41 423 | 424 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.6%2B0.16.40...v0.11.7%2B0.16.41) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/25) 425 | 426 | ### Changed 427 | 428 | - Upgraded [`tb_client` C library] to [0.16.41 version][tb-0.16.41]. ([#68]) 429 | 430 | [#68]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/68 431 | [tb-0.16.41]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.41/CHANGELOG.md#tigerbeetle-01641 432 | 433 | 434 | 435 | 436 | ## [0.11.6+0.16.40] · 2025-05-13 437 | [0.11.6+0.16.40]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.6%2B0.16.40 438 | 439 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.5%2B0.16.39...v0.11.6%2B0.16.40) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/24) 440 | 441 | ### Changed 442 | 443 | - Upgraded [`tb_client` C library] to [0.16.40 version][tb-0.16.40]. ([#67]) 444 | 445 | [#67]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/67 446 | [tb-0.16.40]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.40/CHANGELOG.md#tigerbeetle-01640 447 | 448 | 449 | 450 | 451 | ## [0.11.5+0.16.39] · 2025-05-06 452 | [0.11.5+0.16.39]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.5%2B0.16.39 453 | 454 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.4%2B0.16.38...v0.11.5%2B0.16.39) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/23) 455 | 456 | ### Changed 457 | 458 | - Upgraded [`tb_client` C library] to [0.16.39 version][tb-0.16.39]. ([#66]) 459 | 460 | [#66]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/66 461 | [tb-0.16.39]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.39/CHANGELOG.md#tigerbeetle-01639 462 | 463 | 464 | 465 | 466 | ## [0.11.4+0.16.38] · 2025-04-29 467 | [0.11.4+0.16.38]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.4%2B0.16.38 468 | 469 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.3%2B0.16.37...v0.11.4%2B0.16.38) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/22) 470 | 471 | ### Added 472 | 473 | - `sys` crate: 474 | - `tb_client_init_parameters()` function. ([#65], [tigerbeetle/tigerbeetle#2665]) 475 | - `tb_init_parameters_t` struct. ([#65], [tigerbeetle/tigerbeetle#2665]) 476 | 477 | ### Changed 478 | 479 | - Upgraded [`tb_client` C library] to [0.16.38 version][tb-0.16.38]. ([#65]) 480 | 481 | [#65]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/65 482 | [tb-0.16.38]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.38/CHANGELOG.md#tigerbeetle-01638 483 | [tigerbeetle/tigerbeetle#2665]: https://github.com/tigerbeetle/tigerbeetle/pull/2665 484 | 485 | 486 | 487 | 488 | ## [0.11.3+0.16.37] · 2025-04-22 489 | [0.11.3+0.16.37]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.3%2B0.16.37 490 | 491 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.2%2B0.16.36...v0.11.3%2B0.16.37) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/21) 492 | 493 | ### Changed 494 | 495 | - Upgraded [`tb_client` C library] to [0.16.37 version][tb-0.16.37]. ([#64]) 496 | 497 | [#64]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/64 498 | [tb-0.16.37]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.37/CHANGELOG.md#tigerbeetle-01637 499 | 500 | 501 | 502 | 503 | ## [0.11.2+0.16.36] · 2025-04-15 504 | [0.11.2+0.16.36]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.2%2B0.16.36 505 | 506 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.1%2B0.16.36...v0.11.2%2B0.16.36) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/20) 507 | 508 | ### Fixed 509 | 510 | - Incorrect handling of `sys::tb_client_t` inside `core::Client` resulting in deadlock. ([#63], [#59]) 511 | 512 | [#59]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/issues/59 513 | [#63]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/62 514 | 515 | 516 | 517 | 518 | ## [0.11.1+0.16.36] · 2025-04-15 519 | [0.11.1+0.16.36]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.1%2B0.16.36 520 | 521 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.11.0%2B0.16.35...v0.11.1%2B0.16.36) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/19) 522 | 523 | ### Changed 524 | 525 | - Upgraded [`tb_client` C library] to [0.16.36 version][tb-0.16.36]. ([#62]) 526 | 527 | [#62]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/62 528 | [tb-0.16.36]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.36/CHANGELOG.md#tigerbeetle-01636 529 | 530 | 531 | 532 | 533 | ## [0.11.0+0.16.35] · 2025-04-09 534 | [0.11.0+0.16.35]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.11.0%2B0.16.35 535 | 536 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.10.1%2B0.16.34...v0.11.0%2B0.16.35) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/18) 537 | 538 | ### BC Breaks 539 | 540 | - `sys` crate: 541 | - Shifted numeric representation of values in `TB_OPERATION` enumeration. ([#60], [tigerbeetle/tigerbeetle#2787]) 542 | 543 | ### Added 544 | 545 | - `sys` crate: 546 | - `generated_safe`: 547 | - `EXCLUDED_OPERATION_CODES` constant holding the excluded codes between `MIN_OPERATION_CODE` AND `MIN_OPERATION_CODE`. ([#60]) 548 | 549 | ### Changed 550 | 551 | - Upgraded [`tb_client` C library] to [0.16.35 version][tb-0.16.35]. ([#60]) 552 | 553 | [#60]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/60 554 | [tb-0.16.35]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.35/CHANGELOG.md#tigerbeetle-01635 555 | [tigerbeetle/tigerbeetle#2787]: https://github.com/tigerbeetle/tigerbeetle/pull/2787 556 | 557 | 558 | 559 | 560 | ## [0.10.1+0.16.34] · 2025-04-01 561 | [0.10.1+0.16.34]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.10.1%2B0.16.34 562 | 563 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.10.0%2B0.16.33...v0.10.1%2B0.16.34) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/17) 564 | 565 | ### Changed 566 | 567 | - Upgraded [`tb_client` C library] to [0.16.34 version][tb-0.16.34]. ([#58]) 568 | 569 | [#58]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/58 570 | [tb-0.16.34]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.34/CHANGELOG.md#tigerbeetle-01634 571 | 572 | 573 | 574 | 575 | ## [0.10.0+0.16.33] · 2025-03-25 576 | [0.10.0+0.16.33]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.10.0%2B0.16.33 577 | 578 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.9.3%2B0.16.32...v0.10.0%2B0.16.33) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/16) 579 | 580 | ### BC Breaks 581 | 582 | - `sys` crate: 583 | - Removed `TB_CREATE_TRANSFER_AMOUNT_MUST_NOT_BE_ZERO` value from `TB_CREATE_TRANSFER_RESULT` enumeration. ([#57], [tigerbeetle/tigerbeetle#2824]) 584 | - `generated_safe`: 585 | - Removed `AmountMustNotBeZero` variant from `CreateTransferErrorKind` enum. ([#57], [tigerbeetle/tigerbeetle#2824]) 586 | - `core` crate: 587 | - Removed `AmountMustNotBeZero` variant from `error::CreateTransferErrorKind` enum. ([#57], [tigerbeetle/tigerbeetle#2824]) 588 | - Main crate: 589 | - Removed `AmountMustNotBeZero` variant from `error::CreateTransferErrorKind` enum. ([#57], [tigerbeetle/tigerbeetle#2824]) 590 | 591 | ### Added 592 | 593 | - `sys` crate: 594 | - `generated_safe`: 595 | - `EXCLUDED_CREATE_TRANSFER_ERROR_CODES` constant holding the code of the removed `CreateTransferErrorKind::AmountMustNotBeZero` variant. ([#57]) 596 | 597 | ### Changed 598 | 599 | - Upgraded [`tb_client` C library] to [0.16.33 version][tb-0.16.33]. ([#57]) 600 | 601 | [#57]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/57 602 | [tb-0.16.33]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.33/CHANGELOG.md#tigerbeetle-01633 603 | [tigerbeetle/tigerbeetle#2824]: https://github.com/tigerbeetle/tigerbeetle/pull/2824 604 | 605 | 606 | 607 | 608 | ## [0.9.3+0.16.32] · 2025-03-20 609 | [0.9.3+0.16.32]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.9.3%2B0.16.32 610 | 611 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.9.2%2B0.16.31...v0.9.3%2B0.16.32) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/15) 612 | 613 | ### Changed 614 | 615 | - Upgraded [`tb_client` C library] to [0.16.32 version][tb-0.16.32]. ([#56]) 616 | 617 | [#56]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/56 618 | [tb-0.16.32]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.32/CHANGELOG.md#tigerbeetle-01632 619 | 620 | 621 | 622 | 623 | ## [0.9.2+0.16.31] · 2025-03-20 624 | [0.9.2+0.16.31]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.9.2%2B0.16.31 625 | 626 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.9.1%2B0.16.30...v0.9.2%2B0.16.31) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/14) 627 | 628 | ### Changed 629 | 630 | - Upgraded [`tb_client` C library] to [0.16.31 version][tb-0.16.31]. ([#54]) 631 | 632 | [#54]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/54 633 | [tb-0.16.31]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.31/CHANGELOG.md#tigerbeetle-01631 634 | 635 | 636 | 637 | 638 | ## [0.9.1+0.16.30] · 2025-03-20 639 | [0.9.1+0.16.30]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.9.1%2B0.16.30 640 | 641 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.9.0%2B0.16.29...v0.9.1%2B0.16.30) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/13) 642 | 643 | ### Changed 644 | 645 | - Upgraded [`tb_client` C library] to [0.16.30 version][tb-0.16.30]. ([#52]) 646 | 647 | [#52]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/52 648 | [tb-0.16.30]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.30/CHANGELOG.md#tigerbeetle-01630 649 | 650 | 651 | 652 | 653 | ## [0.9.0+0.16.29] · 2025-03-20 654 | [0.9.0+0.16.29]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.9.0%2B0.16.29 655 | 656 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.8.0%2B0.16.28...v0.9.0%2B0.16.29) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/12) 657 | 658 | ### BC Breaks 659 | 660 | - `sys` crate: 661 | - Renamed `TB_STATUS` enumeration as `TB_INIT_STATUS`. ([#49], [tigerbeetle/tigerbeetle#2742]) 662 | - Transformed `tb_client_t` into struct with `opaque` field. ([#49], [tigerbeetle/tigerbeetle#2742]) 663 | - `tb_packet_t`: ([#49], [tigerbeetle/tigerbeetle#2742]) 664 | - Renamed `tag` field as `user_tag`. 665 | - Renamed `reserved` field as `opaque`. 666 | - `tb_client_deinit()`: ([#49], [tigerbeetle/tigerbeetle#2742]) 667 | - Changed return type to `TB_CLIENT_STATUS`. 668 | - Changed `client` argument to `*mut tb_client_t`. 669 | - `tb_client_submit()`: ([#49], [tigerbeetle/tigerbeetle#2742]) 670 | - Changed return type to `TB_CLIENT_STATUS`. 671 | - Changed `client` argument to `*mut tb_client_t`. 672 | - `tb_client_completion_context()`: ([#49], [tigerbeetle/tigerbeetle#2742]) 673 | - Changed return type to `TB_CLIENT_STATUS`. 674 | - Changed `client` argument to `*mut tb_client_t`. 675 | - Added `completion_ctx_out: *mut usize` argument. 676 | - `tb_client_init()` and `tb_client_init_echo()`: ([#49], [tigerbeetle/tigerbeetle#2742]) 677 | - Removed second `tb_client_t` argument from `completion_callback` (was `on_completion`) argument. 678 | - `core` crate: 679 | - Renamed `Callbacks::on_completion()` as `Callbacks::completion()` to match [`tb_client` C library] naming. ([#49]) 680 | - Removed lifetime parameter from `Packet`. ([#49]) 681 | - Removed `ClientHandle`, `Client::handle()`, `Packet::client_handle()` and `handle` argument of `Packet::new()`. ([#49]) 682 | - Remade `Packet::submit()` into `Client::submit()`. ([#49]) 683 | - Removed `Client::packet()` (use `Packet::new()` instead). ([#49]) 684 | - Main crate: 685 | - Removed lifetime parameter from `Packet`. ([#49]) 686 | 687 | ### Added 688 | 689 | - `sys` crate: 690 | - `TB_OPERATION_GET_EVENTS` value to `TB_OPERATION` enumeration. ([#49], [tigerbeetle/tigerbeetle#2507]) 691 | - `TB_CLIENT_STATUS` enumeration. ([#49], [tigerbeetle/tigerbeetle#2742]) 692 | - `TB_REGISTER_LOG_CALLBACK_STATUS` enumeration. ([#49], [tigerbeetle/tigerbeetle#2742]) 693 | - `TB_LOG_LEVEL` enumeration. ([#49], [tigerbeetle/tigerbeetle#2742]) 694 | - `tb_client_register_log_callback()` function. ([#49], [tigerbeetle/tigerbeetle#2742]) 695 | - `core` crate: 696 | - `OperationKind::GetEvents` variant. ([#49]) 697 | 698 | ### Changed 699 | 700 | - Upgraded [`tb_client` C library] to [0.16.29 version][tb-0.16.29]. ([#49]) 701 | 702 | [#49]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/49 703 | [tb-0.16.29]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.29/CHANGELOG.md#tigerbeetle-01629 704 | [tigerbeetle/tigerbeetle#2507]: https://github.com/tigerbeetle/tigerbeetle/pull/2507 705 | [tigerbeetle/tigerbeetle#2742]: https://github.com/tigerbeetle/tigerbeetle/pull/2742 706 | 707 | 708 | 709 | 710 | ## [0.8.0+0.16.28] · 2025-02-18 711 | [0.8.0+0.16.28]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.8.0%2B0.16.28 712 | 713 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.5%2B0.16.27...v0.8.0%2B0.16.28) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/11) 714 | 715 | ### BC Breaks 716 | 717 | - `sys` crate: 718 | - `tb_packet_t`: ([#48], [tigerbeetle/tigerbeetle#2728]) 719 | - `next`, `batch_next`, `batch_tail`, `batch_size` and `batch_allowed` fields were hidden into opaque `reserved` field. 720 | - Added `tag` field. 721 | 722 | ### Changed 723 | 724 | - Upgraded [`tb_client` C library] to [0.16.28 version][tb-0.16.28]. ([#48]) 725 | 726 | [#48]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/48 727 | [tb-0.16.28]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.28/CHANGELOG.md#tigerbeetle-01628 728 | [tigerbeetle/tigerbeetle#2728]: https://github.com/tigerbeetle/tigerbeetle/pull/2728 729 | 730 | 731 | 732 | 733 | ## [0.7.5+0.16.27] · 2025-02-11 734 | [0.7.5+0.16.27]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.5%2B0.16.27 735 | 736 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.4%2B0.16.26...v0.7.5%2B0.16.27) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/10) 737 | 738 | ### Changed 739 | 740 | - Upgraded [`tb_client` C library] to [0.16.27 version][tb-0.16.27]. ([#47]) 741 | 742 | [#47]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/47 743 | [tb-0.16.27]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.27/CHANGELOG.md#tigerbeetle-01627 744 | 745 | 746 | 747 | 748 | ## [0.7.4+0.16.26] · 2025-02-04 749 | [0.7.4+0.16.26]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.4%2B0.16.26 750 | 751 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.3%2B0.16.25...v0.7.4%2B0.16.26) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/9) 752 | 753 | ### Changed 754 | 755 | - Upgraded [`tb_client` C library] to [0.16.26 version][tb-0.16.26]. ([#46]) 756 | 757 | [#46]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/46 758 | [tb-0.16.26]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.26/CHANGELOG.md#tigerbeetle-01626 759 | 760 | 761 | 762 | 763 | ## [0.7.3+0.16.25] · 2025-01-28 764 | [0.7.3+0.16.25]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.3%2B0.16.25 765 | 766 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.2%2B0.16.23...v0.7.3%2B0.16.25) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/8) 767 | 768 | ### Changed 769 | 770 | - Upgraded [`tb_client` C library] to [0.16.25 version][tb-0.16.25]. ([#45]) 771 | 772 | [#45]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/45 773 | [tb-0.16.25]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.25/CHANGELOG.md#tigerbeetle-01625 774 | 775 | 776 | 777 | 778 | ## [0.7.2+0.16.23] · 2025-01-21 779 | [0.7.2+0.16.23]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.2%2B0.16.23 780 | 781 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.1%2B0.16.21...v0.7.2%2B0.16.23) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/7) 782 | 783 | ### Changed 784 | 785 | - Upgraded [`tb_client` C library] to [0.16.23 version][tb-0.16.23]. ([#44]) 786 | 787 | [#44]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/44 788 | [tb-0.16.23]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.23/CHANGELOG.md#tigerbeetle-01623 789 | 790 | 791 | 792 | 793 | ## [0.7.1+0.16.21] · 2025-01-14 794 | [0.7.1+0.16.21]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.1%2B0.16.21 795 | 796 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.7.0%2B0.16.20...v0.7.1%2B0.16.21) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/6) 797 | 798 | ### Changed 799 | 800 | - Upgraded [`tb_client` C library] to [0.16.21 version][tb-0.16.21]. ([#43]) 801 | 802 | [#43]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/43 803 | [tb-0.16.21]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.21/CHANGELOG.md#tigerbeetle-01621 804 | 805 | 806 | 807 | 808 | ## [0.7.0+0.16.20] · 2024-12-30 809 | [0.7.0+0.16.20]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.7.0%2B0.16.20 810 | 811 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.6.1%2B0.16.19...v0.7.0%2B0.16.20) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/4) 812 | 813 | ### BC Breaks 814 | 815 | - Removed MIT license. ([#41]) 816 | 817 | ### Changed 818 | 819 | - Upgraded [`tb_client` C library] to [0.16.20 version][tb-0.16.20]. ([#42]) 820 | 821 | [#41]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/41 822 | [#42]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/42 823 | [tb-0.16.20]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.20/CHANGELOG.md#tigerbeetle-01620 824 | 825 | 826 | 827 | 828 | ## [0.6.1+0.16.19] · 2024-12-25 829 | [0.6.1+0.16.19]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.6.1%2B0.16.19 830 | 831 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.6.0%2B0.16.17...v0.6.1%2B0.16.19) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/3) 832 | 833 | ### Added 834 | 835 | - `core` and main crates: 836 | - `error::SendErrorKind::ClientReleaseTooLow` and `error::SendErrorKind::ClientReleaseTooHigh` variants. ([#40], [tigerbeetle/tigerbeetle#2552]) 837 | - `sys` crate: 838 | - `TB_PACKET_STATUS::TB_PACKET_CLIENT_RELEASE_TOO_LOW` and `TB_PACKET_STATUS::TB_PACKET_CLIENT_RELEASE_TOO_HIGH` constants. ([#40], [tigerbeetle/tigerbeetle#2552]) 839 | - `PacketStatusErrorKind::ClientReleaseTooLow` and `PacketStatusErrorKind::ClientReleaseTooHigh` variants. ([#40], [tigerbeetle/tigerbeetle#2552]) 840 | 841 | ### Changed 842 | 843 | - Upgraded [`tb_client` C library] to [0.16.19 version][tb-0.16.19]. ([#40]) 844 | 845 | [#40]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/40 846 | [tb-0.16.19]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.19/CHANGELOG.md#tigerbeetle-01619 847 | [tigerbeetle/tigerbeetle#2552]: https://github.com/tigerbeetle/tigerbeetle/pull/2552 848 | 849 | 850 | 851 | 852 | ## [0.6.0+0.16.17] · 2024-12-19 853 | [0.6.0+0.16.17]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.6.0%2B0.16.17 854 | 855 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.5.0%2B0.16.11...v0.6.0%2B0.16.17) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/2) 856 | 857 | ### BC Breaks 858 | 859 | - Upgraded [`tb_client` C library] to [0.16.17 version][tb-0.16.17]: ([#38]) 860 | - Replaced `payload` argument with `reply` in `core::Callbacks::on_competion()` to provide cluster `timestamp` of `Reply` generation. ([tigerbeetle/tigerbeetle#2481]) 861 | - Replaced `TIGERBEETLE_LOG_LEVEL` build time env var with `TB_CLIENT_DEBUG` one, since `config-log-level` build option was removed, but no FFI yet added for configuring runtime log filtering. ([tigerbeetle/tigerbeetle#2539]) 862 | 863 | ### Added 864 | 865 | - `SendErrorKind::ClientEvicted` variant. ([#38], [tigerbeetle/tigerbeetle#2484]) 866 | - `id()` function generating [TigerBeetle Time-Based Identifiers](https://github.com/tigerbeetle/tigerbeetle/blob/0.16.17/docs/coding/data-modeling.md#tigerbeetle-time-based-identifiers-recommended). ([#39]) 867 | 868 | [#38]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/38 869 | [#39]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/39 870 | [tb-0.16.17]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.17/CHANGELOG.md#tigerbeetle-01617 871 | [tigerbeetle/tigerbeetle#2539]: https://github.com/tigerbeetle/tigerbeetle/pull/2539 872 | [tigerbeetle/tigerbeetle#2481]: https://github.com/tigerbeetle/tigerbeetle/pull/2481 873 | [tigerbeetle/tigerbeetle#2484]: https://github.com/tigerbeetle/tigerbeetle/pull/2484 874 | 875 | 876 | 877 | 878 | ## [0.5.0+0.16.11] · 2024-12-02 879 | [0.5.0+0.16.11]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.5.0%2B0.16.11 880 | 881 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.4.1%2B0.15.3...v0.5.0%2B0.16.11) | [Milestone](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/milestone/1) 882 | 883 | ### BC Breaks 884 | 885 | - Upgraded [`tb_client` C library] to [0.16.11 version][tb-0.16.11]. ([#24], [#19], [#18]) 886 | - Removed `concurrency_max` argument from `Client::new()`, `Client::with_callback()` and `Client::with_callback_unchecked()` methods. ([#24], [#19]) 887 | - Replaced `Client::acquire()` and `ClientHandle::acquire()` methods with `Client::packet()` and `Packet::new()`. ([#24], [#19], [#34]) 888 | - Removed `error::AcquirePacketError` type. ([#24], [#19]) 889 | 890 | ### Added 891 | 892 | - `TIGERBEETLE_LOG_LEVEL` env var for setting `config-log-level` when building (default is `info`). ([#24], [#19]) 893 | - `QueryFilter` and `query_filter::Raw` types. ([#26]) 894 | - `Client::query_accounts()` and `Client::query_transfers()` methods. ([#26]) 895 | 896 | ### Fixed 897 | 898 | - Broken builds inside environments without [Git] (like [Docker] image). ([#23], [#20]) 899 | 900 | [#18]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/issues/18 901 | [#19]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/19 902 | [#20]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/issues/20 903 | [#23]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/23 904 | [#24]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/24 905 | [#26]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/26 906 | [#34]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/pull/34 907 | [tb-0.16.11]: https://github.com/tigerbeetle/tigerbeetle/blob/0.16.11/CHANGELOG.md#tigerbeetle-01611 908 | 909 | 910 | 911 | 912 | ## [0.4.1+0.15.3] · 2024-07-28 913 | [0.4.1+0.15.3]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.4.1%2B0.15.3 914 | 915 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.4.0%2B0.15.4...v0.4.1%2B0.15.3) 916 | 917 | See the [release notes][release-0.4.1+0.15.3]. 918 | 919 | [release-0.4.1+0.15.3]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/releases/tag/v0.4.1%2B0.15.3 920 | 921 | 922 | 923 | 924 | ## [0.4.0+0.15.3] · 2024-07-13 925 | [0.4.0+0.15.3]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.4.0%2B0.15.4 926 | 927 | [Diff](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/v0.3.0%2B0.13.133...v0.4.0%2B0.15.4) 928 | 929 | See the [release notes][release-0.4.0+0.15.3]. 930 | 931 | [release-0.4.0+0.15.3]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/releases/tag/v0.4.0%2B0.15.4 932 | 933 | 934 | 935 | 936 | ## [0.3.0+0.13.133] and prior 937 | [0.3.0+0.13.133]: https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/tree/v0.3.0%2B0.13.133 938 | 939 | See [Git log](https://github.com/tigerbeetle-rust/tigerbeetle-unofficial/compare/a4994b2da3914352b8d64adae0535189b4bc7b27...v0.3.0%2B0.13.133). 940 | 941 | 942 | 943 | 944 | [`tb_client` C library]: https://github.com/tigerbeetle/tigerbeetle/tree/main/src/clients/c 945 | [Docker]: https://www.docker.com 946 | [Git]: https://git-scm.com 947 | [MSRV]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field 948 | [Semantic Versioning 2.0.0]: https://semver.org 949 | --------------------------------------------------------------------------------