├── .githooks ├── pre-commit ├── pre-push └── util.sh ├── .github ├── FUNDING.yml ├── pull_request_template.md ├── renovate.json └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── CLAUDE.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── zlink-codegen ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── zlink-core ├── Cargo.toml ├── README.md └── src │ ├── call.rs │ ├── connection │ ├── chain │ │ ├── mod.rs │ │ └── reply_stream.rs │ ├── mod.rs │ ├── read_connection.rs │ ├── socket.rs │ └── write_connection.rs │ ├── error.rs │ ├── lib.rs │ ├── log.rs │ ├── reply.rs │ └── server │ ├── listener.rs │ ├── mod.rs │ ├── select_all.rs │ └── service.rs ├── zlink-macros ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── zlink-micro ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── zlink-tokio ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── notified.rs │ └── unix │ ├── listener.rs │ ├── mod.rs │ └── stream.rs ├── zlink-usb ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── zlink ├── Cargo.toml ├── README.md ├── examples └── resolved.rs ├── src └── lib.rs └── tests └── lowlevel-ftl.rs /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GITHOOKS_DIR=$( cd -- "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd ) 3 | source $GITHOOKS_DIR/util.sh 4 | 5 | ensure_rustup_installed 6 | ensure_rustfmt_installed 7 | 8 | check_formatting 9 | -------------------------------------------------------------------------------- /.githooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GITHOOKS_DIR=$( cd -- "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd ) 3 | source $GITHOOKS_DIR/util.sh 4 | 5 | ensure_rustup_installed 6 | ensure_clippy_installed 7 | 8 | check_clippy 9 | -------------------------------------------------------------------------------- /.githooks/util.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Utility functions for git hook scripts. 4 | 5 | if test -t 1 && test -n "$(tput colors)" && test "$(tput colors)" -ge 8; then 6 | bold="$(tput bold)" 7 | normal="$(tput sgr0)" 8 | green="$(tput setaf 2)" 9 | red="$(tput setaf 1)" 10 | blue="$(tput setaf 4)" 11 | 12 | function hook_failure { 13 | echo "${red}${bold}FAILED:${normal} ${1}${normal}" 14 | exit 1 15 | } 16 | 17 | function hook_info { 18 | echo "${blue}${1}${normal}" 19 | } 20 | 21 | function hook_success { 22 | echo "${green}${bold}SUCCESS:${normal} ${1}${normal}" 23 | echo 24 | echo 25 | } 26 | 27 | else 28 | function hook_failure { 29 | echo "FAILED: ${1}" 30 | exit 1 31 | } 32 | 33 | function hook_info { 34 | echo "{$1}" 35 | } 36 | 37 | function hook_success { 38 | echo "SUCCESS: ${1}" 39 | echo 40 | echo 41 | } 42 | fi 43 | 44 | function ensure_rustup_installed() { 45 | hook_info "📦️ Ensuring that rustup is installed" 46 | if ! which rustup &> /dev/null; then 47 | curl https://sh.rustup.rs -sSf | sh -s -- -y 48 | export PATH=$PATH:$HOME/.cargo/bin 49 | if ! which rustup &> /dev/null; then 50 | hook_failure "Failed to install rustup" 51 | else 52 | hook_success "rustup installed." 53 | fi 54 | else 55 | hook_success "rustup is already installed." 56 | fi 57 | } 58 | 59 | function ensure_rustfmt_installed() { 60 | hook_info "📦️ Ensuring that nightly rustfmt is installed" 61 | if ! rustup component list --toolchain nightly|grep 'rustfmt-preview.*(installed)' &> /dev/null; then 62 | rustup component add rustfmt-preview --toolchain nightly 63 | hook_success "rustfmt installed." 64 | else 65 | hook_success "rustfmt is already installed." 66 | fi 67 | } 68 | 69 | function ensure_clippy_installed() { 70 | hook_info "📦️ Ensuring that clippy is installed" 71 | if ! rustup component list --toolchain stable|grep 'clippy.*(installed)' &> /dev/null; then 72 | rustup component add clippy 73 | hook_success "clippy installed." 74 | else 75 | hook_success "clippy is already installed." 76 | fi 77 | } 78 | 79 | function check_formatting() { 80 | hook_info "🎨 Running 'cargo +nightly fmt -- --check'" 81 | cargo +nightly fmt -- --check \ 82 | && hook_success "Project is formatted" \ 83 | || hook_failure "Cargo format detected errors." 84 | } 85 | 86 | function check_clippy() { 87 | hook_info "🔍 Running 'cargo clippy -- -D warnings'" 88 | cargo clippy -- -D warnings \ 89 | && hook_success "Clippy detected no issues" \ 90 | || hook_failure "Cargo clippy detected errors." 91 | } 92 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: zeenix -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageRules": [ 3 | { 4 | "matchManagers": [ 5 | "github-actions" 6 | ], 7 | "commitMessageAction": "⬆️ Update" 8 | }, 9 | { 10 | "matchManagers": [ 11 | "cargo" 12 | ], 13 | "commitMessageAction": "⬆️ Update", 14 | "commitMessageTopic": "{{depName}}", 15 | "lockFileMaintenance": { 16 | "enabled": true 17 | } 18 | }, 19 | { 20 | "matchUpdateTypes": [ 21 | "patch", 22 | "pin", 23 | "digest" 24 | ], 25 | "commitMessageAction": "⬆️ micro: Update", 26 | "automerge": true, 27 | "rebaseWhen": "conflicted" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*-*.*.*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | ref: ${{ github.ref }} 17 | - uses: spenserblack/actions-tag-to-release@main 18 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Lint, Build and Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | MSRV: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | target: [x86_64-unknown-linux-gnu, x86_64-apple-darwin, x86_64-unknown-freebsd, x86_64-unknown-netbsd] 14 | env: 15 | RUSTFLAGS: -D warnings 16 | MSRV: 1.82.0 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: dtolnay/rust-toolchain@master 20 | with: 21 | toolchain: ${{ env.MSRV }} 22 | targets: ${{ matrix.TARGET }} 23 | components: rustfmt 24 | - uses: Swatinem/rust-cache@v2 25 | - name: Check build with MSRV 26 | run: | 27 | cargo --locked check --target ${{ matrix.TARGET }} 28 | 29 | fmt: 30 | runs-on: ubuntu-latest 31 | env: 32 | RUSTFLAGS: -D warnings 33 | RUST_BACKTRACE: full 34 | RUST_LOG: trace 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: dtolnay/rust-toolchain@master 38 | with: 39 | # We use some nightly fmt options. 40 | toolchain: nightly 41 | components: rustfmt 42 | - uses: Swatinem/rust-cache@v2 43 | - name: Check formatting 44 | run: | 45 | cargo --locked fmt -- --check 46 | 47 | clippy: 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | target: [x86_64-unknown-linux-gnu, x86_64-apple-darwin, x86_64-unknown-freebsd, x86_64-unknown-netbsd] 52 | env: 53 | RUSTFLAGS: -D warnings 54 | RUST_BACKTRACE: full 55 | RUST_LOG: trace 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: dtolnay/rust-toolchain@master 59 | with: 60 | toolchain: stable 61 | components: clippy 62 | targets: x86_64-apple-darwin, x86_64-unknown-freebsd, x86_64-unknown-netbsd 63 | - uses: Swatinem/rust-cache@v2 64 | - name: Catch common mistakes and unwrap calls 65 | run: | 66 | cargo --locked clippy --target ${{ matrix.target }} -- -D warnings 67 | 68 | linux_test: 69 | runs-on: ubuntu-latest 70 | needs: [fmt, clippy] 71 | env: 72 | RUSTFLAGS: -D warnings 73 | RUST_BACKTRACE: full 74 | RUST_LOG: trace 75 | steps: 76 | - uses: actions/checkout@v4 77 | - uses: dtolnay/rust-toolchain@master 78 | with: 79 | toolchain: stable 80 | - uses: Swatinem/rust-cache@v2 81 | - name: Build and Test 82 | run: | 83 | cargo test --release 84 | cargo test -p zlink-core --release --no-default-features --features embedded 85 | 86 | doc_build: 87 | runs-on: ubuntu-latest 88 | env: 89 | RUSTDOCFLAGS: -D warnings 90 | steps: 91 | - uses: actions/checkout@v4 92 | - uses: dtolnay/rust-toolchain@master 93 | with: 94 | toolchain: stable 95 | - uses: Swatinem/rust-cache@v2 96 | - name: Check documentation build 97 | run: cargo --locked doc --all-features 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/target 3 | settings.json 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | comment_width = 100 3 | wrap_comments = true 4 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Development Commands 6 | 7 | ### Testing 8 | ```bash 9 | # Run the full test suite, including doc tests and compile-tests 10 | cargo test --all-features 11 | 12 | # Run tests for a specific package 13 | cargo test -p zlink-core 14 | cargo test -p zlink-tokio 15 | ``` 16 | 17 | ### Code Quality 18 | ```bash 19 | # Format code (uses nightly rustfmt) 20 | cargo +nightly fmt --all 21 | 22 | # Run clippy with warnings as errors 23 | cargo clippy -- -D warnings 24 | 25 | # Check all features compile 26 | cargo check --all-features 27 | ``` 28 | 29 | ### Git Hooks Setup 30 | ```bash 31 | # Enable git hooks for automatic formatting and clippy checks 32 | cp .githooks/* .git/hooks/ 33 | ``` 34 | 35 | ## Architecture Overview 36 | 37 | This is a Rust workspace implementing an asynchronous no-std-compatible Varlink IPC library. The architecture is modular with clear separation of concerns: 38 | 39 | ### Core Architecture 40 | - **zlink-core**: No-std/no-alloc foundation providing core APIs. Not used directly. 41 | - **zlink**: Main unified API crate that re-exports appropriate subcrates based on cargo features 42 | - **zlink-tokio**: Tokio runtime integration and transport implementations 43 | - **zlink-usb** + **zlink-micro**: Enable USB-based IPC between Linux hosts and microcontrollers 44 | 45 | ### Key Components 46 | - **Connection**: Low-level API for message send/receive with unique IDs for read/write halves 47 | - **Server**: Listens for connections and handles method calls via services 48 | - **Service**: Trait defining IPC service implementations 49 | - **Call/Reply**: Core message types for IPC communication 50 | 51 | ### Feature System 52 | - `std` feature: Standard library support with serde_json 53 | - `embedded` feature: No-std support with serde-json-core and defmt logging 54 | - I/O buffer size features: `io-buffer-2kb` (default), `io-buffer-4kb`, `io-buffer-16kb`, `io-buffer-1mb` 55 | 56 | ### Development Patterns 57 | - Uses workspace-level package metadata (edition, rust-version, license, repository) 58 | - Supports both std and no_std environments through feature flags 59 | - Leverages mayheap for heap/heapless abstraction 60 | - Uses pin-project-lite for async/await support 61 | 62 | ### Code Style 63 | - Follows GNOME commit message guidelines with emoji prefixes 64 | - Atomic commits preferred (one logical change per commit) 65 | - Package prefixes in commit messages 66 | - Force-push workflow for addressing review comments -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We welcome contributions from everyone in the form of suggestions, bug reports, pull requests, and 4 | feedback. This document gives some guidance if you are thinking of helping us. 5 | 6 | Please reach out here in a Github issue, or in the 7 | [#varlink:matrix.org](https://matrix.to/#/#varlink:matrix.org) Matrix room if we can do anything to help 8 | you contribute. 9 | 10 | ## Submitting bug reports and feature requests 11 | 12 | You can create issues [here](https://github.com/zeenix/zlink/issues/new). When reporting a bug or 13 | asking for help, please include enough details so that the people helping you can reproduce the 14 | behavior you are seeing. For some tips on how to approach this, read about how to produce a 15 | [Minimal, Complete, and Verifiable Example](https://stackoverflow.com/help/mcve). 16 | 17 | When making a feature request, please make it clear what problem you intend to solve with the 18 | feature, any ideas for how the crate in question could support solving that problem, any possible 19 | alternatives, and any disadvantages. 20 | 21 | ## Submitting Pull Requests 22 | 23 | Same rules apply here as for bug reports and feature requests. Plus: 24 | 25 | * We prefer atomic commits. Please read 26 | [this excellent blog post](https://www.aleksandrhovhannisyan.com/blog/atomic-git-commits/) for 27 | more information, including the rationale. For larger changes addressing several packages 28 | consider splitting your pull request, using a single commit for each package changed. 29 | * Please try your best to follow [these guidelines](https://handbook.gnome.org/development/commit-messages.html) for 30 | commit messages. 31 | * We also prefer adding [emoji prefixes to commit messages](https://gitmoji.carloscuesta.me/). Since 32 | the `gitmoji` CLI tool can be very [slow](https://github.com/zeenix/gimoji#rationale), we 33 | recommend using [`gimoji`](https://github.com/zeenix/gimoji) instead. You can also pick an emoji 34 | direcitly from [here](https://gitmoji.dev/). 35 | * Add a prefix indicating the packages being changed. 36 | * Add details to each commit about the changes it contains. PR description is for summarizing the 37 | overall changes in the PR, while commit logs are for describing the specific changes of the 38 | commit in question. 39 | * When addressesing review comments, fix the existing commits in the PR (rather than adding 40 | additional commits) and force push (as in `git push -f`) to your branch. You may find 41 | [`git-absorb`](https://github.com/tummychow/git-absorb) and 42 | [`git-revise`](https://github.com/mystor/git-revise) extremely useful, especially if you're not 43 | very familiar with interactive rebasing and modifying commits in git. 44 | 45 | ### Legal Notice 46 | 47 | When contributing to this project, you **implicitly** declare that: 48 | 49 | * you have authored 100% of the content, 50 | * you have the necessary rights to the content, and 51 | * you agree to providing the content under the [project's license](LICENSE). 52 | 53 | ## Running the test suite 54 | 55 | We encourage you to check that the test suite passes locally before submitting a pull request with 56 | your changes. If anything does not pass, typically it will be easier to iterate and fix it locally 57 | than waiting for the CI servers to run tests for you. 58 | 59 | ```sh 60 | # Run the full test suite, including doc test and compile-tests 61 | cargo test --all-features 62 | ``` 63 | 64 | Also please ensure that code is formatted correctly by running: 65 | 66 | ```sh 67 | cargo +nightly fmt --all 68 | ``` 69 | 70 | and clippy doesn't see anything wrong with the code: 71 | 72 | ```sh 73 | cargo clippy -- -D warnings 74 | ``` 75 | 76 | Please note that there are times when clippy is wrong and you know what you are doing. In such 77 | cases, it's acceptable to tell clippy to 78 | [ignore the specific error or warning in the code](https://github.com/rust-lang/rust-clippy#allowingdenying-lints). 79 | 80 | If you intend to contribute often or think that's very likely, we recommend you setup the git hook 81 | scripts contained within this repository. You can enable them with: 82 | 83 | ```sh 84 | cp .githooks/* .git/hooks/ 85 | ``` 86 | 87 | ## Conduct 88 | 89 | We follow the 90 | [Rust Code of Conduct](https://www.rust-lang.org/conduct.html). For escalation or moderation issues 91 | please contact [Zeeshan](mailto:zeeshanak@gnome.org) instead of the Rust moderation team. 92 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys 0.59.0", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.7" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell", 76 | "windows-sys 0.59.0", 77 | ] 78 | 79 | [[package]] 80 | name = "backtrace" 81 | version = "0.3.74" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 84 | dependencies = [ 85 | "addr2line", 86 | "cfg-if", 87 | "libc", 88 | "miniz_oxide", 89 | "object", 90 | "rustc-demangle", 91 | "windows-targets", 92 | ] 93 | 94 | [[package]] 95 | name = "bitflags" 96 | version = "1.3.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 99 | 100 | [[package]] 101 | name = "byteorder" 102 | version = "1.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 105 | 106 | [[package]] 107 | name = "bytes" 108 | version = "1.10.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 111 | 112 | [[package]] 113 | name = "cfg-if" 114 | version = "1.0.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 117 | 118 | [[package]] 119 | name = "colorchoice" 120 | version = "1.0.3" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 123 | 124 | [[package]] 125 | name = "defmt" 126 | version = "1.0.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" 129 | dependencies = [ 130 | "bitflags", 131 | "defmt-macros", 132 | ] 133 | 134 | [[package]] 135 | name = "defmt-macros" 136 | version = "1.0.1" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" 139 | dependencies = [ 140 | "defmt-parser", 141 | "proc-macro-error2", 142 | "proc-macro2", 143 | "quote", 144 | "syn", 145 | ] 146 | 147 | [[package]] 148 | name = "defmt-parser" 149 | version = "1.0.0" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" 152 | dependencies = [ 153 | "thiserror", 154 | ] 155 | 156 | [[package]] 157 | name = "env_filter" 158 | version = "0.1.3" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 161 | dependencies = [ 162 | "log", 163 | ] 164 | 165 | [[package]] 166 | name = "env_logger" 167 | version = "0.11.8" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 170 | dependencies = [ 171 | "anstream", 172 | "anstyle", 173 | "env_filter", 174 | "log", 175 | ] 176 | 177 | [[package]] 178 | name = "futures-core" 179 | version = "0.3.31" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 182 | 183 | [[package]] 184 | name = "futures-macro" 185 | version = "0.3.31" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 188 | dependencies = [ 189 | "proc-macro2", 190 | "quote", 191 | "syn", 192 | ] 193 | 194 | [[package]] 195 | name = "futures-sink" 196 | version = "0.3.31" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 199 | 200 | [[package]] 201 | name = "futures-task" 202 | version = "0.3.31" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 205 | 206 | [[package]] 207 | name = "futures-util" 208 | version = "0.3.31" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 211 | dependencies = [ 212 | "futures-core", 213 | "futures-macro", 214 | "futures-task", 215 | "pin-project-lite", 216 | "pin-utils", 217 | ] 218 | 219 | [[package]] 220 | name = "gimli" 221 | version = "0.31.1" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 224 | 225 | [[package]] 226 | name = "hash32" 227 | version = "0.3.1" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 230 | dependencies = [ 231 | "byteorder", 232 | ] 233 | 234 | [[package]] 235 | name = "heapless" 236 | version = "0.8.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 239 | dependencies = [ 240 | "hash32", 241 | "serde", 242 | "stable_deref_trait", 243 | ] 244 | 245 | [[package]] 246 | name = "is_terminal_polyfill" 247 | version = "1.70.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 250 | 251 | [[package]] 252 | name = "itoa" 253 | version = "1.0.14" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 256 | 257 | [[package]] 258 | name = "lazy_static" 259 | version = "1.5.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 262 | 263 | [[package]] 264 | name = "libc" 265 | version = "0.2.170" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 268 | 269 | [[package]] 270 | name = "log" 271 | version = "0.4.27" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 274 | 275 | [[package]] 276 | name = "matchers" 277 | version = "0.1.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 280 | dependencies = [ 281 | "regex-automata 0.1.10", 282 | ] 283 | 284 | [[package]] 285 | name = "mayheap" 286 | version = "0.2.0" 287 | source = "git+https://github.com/zeenix/mayheap#cc19d195121018d25ce43f9be08fb328551d247f" 288 | dependencies = [ 289 | "heapless", 290 | "serde", 291 | ] 292 | 293 | [[package]] 294 | name = "memchr" 295 | version = "2.7.4" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 298 | 299 | [[package]] 300 | name = "miniz_oxide" 301 | version = "0.8.5" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 304 | dependencies = [ 305 | "adler2", 306 | ] 307 | 308 | [[package]] 309 | name = "mio" 310 | version = "1.0.3" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 313 | dependencies = [ 314 | "libc", 315 | "wasi", 316 | "windows-sys 0.52.0", 317 | ] 318 | 319 | [[package]] 320 | name = "nu-ansi-term" 321 | version = "0.46.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 324 | dependencies = [ 325 | "overload", 326 | "winapi", 327 | ] 328 | 329 | [[package]] 330 | name = "object" 331 | version = "0.36.7" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 334 | dependencies = [ 335 | "memchr", 336 | ] 337 | 338 | [[package]] 339 | name = "once_cell" 340 | version = "1.21.3" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 343 | 344 | [[package]] 345 | name = "overload" 346 | version = "0.1.1" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 349 | 350 | [[package]] 351 | name = "pin-project-lite" 352 | version = "0.2.16" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 355 | 356 | [[package]] 357 | name = "pin-utils" 358 | version = "0.1.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 361 | 362 | [[package]] 363 | name = "proc-macro-error-attr2" 364 | version = "2.0.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 367 | dependencies = [ 368 | "proc-macro2", 369 | "quote", 370 | ] 371 | 372 | [[package]] 373 | name = "proc-macro-error2" 374 | version = "2.0.1" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 377 | dependencies = [ 378 | "proc-macro-error-attr2", 379 | "proc-macro2", 380 | "quote", 381 | "syn", 382 | ] 383 | 384 | [[package]] 385 | name = "proc-macro2" 386 | version = "1.0.93" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 389 | dependencies = [ 390 | "unicode-ident", 391 | ] 392 | 393 | [[package]] 394 | name = "quote" 395 | version = "1.0.38" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 398 | dependencies = [ 399 | "proc-macro2", 400 | ] 401 | 402 | [[package]] 403 | name = "regex" 404 | version = "1.11.1" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 407 | dependencies = [ 408 | "aho-corasick", 409 | "memchr", 410 | "regex-automata 0.4.9", 411 | "regex-syntax 0.8.5", 412 | ] 413 | 414 | [[package]] 415 | name = "regex-automata" 416 | version = "0.1.10" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 419 | dependencies = [ 420 | "regex-syntax 0.6.29", 421 | ] 422 | 423 | [[package]] 424 | name = "regex-automata" 425 | version = "0.4.9" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 428 | dependencies = [ 429 | "aho-corasick", 430 | "memchr", 431 | "regex-syntax 0.8.5", 432 | ] 433 | 434 | [[package]] 435 | name = "regex-syntax" 436 | version = "0.6.29" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 439 | 440 | [[package]] 441 | name = "regex-syntax" 442 | version = "0.8.5" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 445 | 446 | [[package]] 447 | name = "rustc-demangle" 448 | version = "0.1.24" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 451 | 452 | [[package]] 453 | name = "ryu" 454 | version = "1.0.19" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 457 | 458 | [[package]] 459 | name = "serde" 460 | version = "1.0.219" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 463 | dependencies = [ 464 | "serde_derive", 465 | ] 466 | 467 | [[package]] 468 | name = "serde-json-core" 469 | version = "0.6.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "5b81787e655bd59cecadc91f7b6b8651330b2be6c33246039a65e5cd6f4e0828" 472 | dependencies = [ 473 | "heapless", 474 | "ryu", 475 | "serde", 476 | ] 477 | 478 | [[package]] 479 | name = "serde_derive" 480 | version = "1.0.219" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 483 | dependencies = [ 484 | "proc-macro2", 485 | "quote", 486 | "syn", 487 | ] 488 | 489 | [[package]] 490 | name = "serde_json" 491 | version = "1.0.140" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 494 | dependencies = [ 495 | "itoa", 496 | "memchr", 497 | "ryu", 498 | "serde", 499 | ] 500 | 501 | [[package]] 502 | name = "serde_repr" 503 | version = "0.1.20" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 506 | dependencies = [ 507 | "proc-macro2", 508 | "quote", 509 | "syn", 510 | ] 511 | 512 | [[package]] 513 | name = "sharded-slab" 514 | version = "0.1.7" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 517 | dependencies = [ 518 | "lazy_static", 519 | ] 520 | 521 | [[package]] 522 | name = "socket2" 523 | version = "0.5.8" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 526 | dependencies = [ 527 | "libc", 528 | "windows-sys 0.52.0", 529 | ] 530 | 531 | [[package]] 532 | name = "stable_deref_trait" 533 | version = "1.2.0" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 536 | 537 | [[package]] 538 | name = "syn" 539 | version = "2.0.98" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 542 | dependencies = [ 543 | "proc-macro2", 544 | "quote", 545 | "unicode-ident", 546 | ] 547 | 548 | [[package]] 549 | name = "test-log" 550 | version = "0.2.17" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" 553 | dependencies = [ 554 | "env_logger", 555 | "test-log-macros", 556 | "tracing-subscriber", 557 | ] 558 | 559 | [[package]] 560 | name = "test-log-macros" 561 | version = "0.2.17" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" 564 | dependencies = [ 565 | "proc-macro2", 566 | "quote", 567 | "syn", 568 | ] 569 | 570 | [[package]] 571 | name = "thiserror" 572 | version = "2.0.12" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 575 | dependencies = [ 576 | "thiserror-impl", 577 | ] 578 | 579 | [[package]] 580 | name = "thiserror-impl" 581 | version = "2.0.12" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 584 | dependencies = [ 585 | "proc-macro2", 586 | "quote", 587 | "syn", 588 | ] 589 | 590 | [[package]] 591 | name = "thread_local" 592 | version = "1.1.8" 593 | source = "registry+https://github.com/rust-lang/crates.io-index" 594 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 595 | dependencies = [ 596 | "cfg-if", 597 | "once_cell", 598 | ] 599 | 600 | [[package]] 601 | name = "tokio" 602 | version = "1.45.1" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 605 | dependencies = [ 606 | "backtrace", 607 | "bytes", 608 | "libc", 609 | "mio", 610 | "pin-project-lite", 611 | "socket2", 612 | "tokio-macros", 613 | "tracing", 614 | "windows-sys 0.52.0", 615 | ] 616 | 617 | [[package]] 618 | name = "tokio-macros" 619 | version = "2.5.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 622 | dependencies = [ 623 | "proc-macro2", 624 | "quote", 625 | "syn", 626 | ] 627 | 628 | [[package]] 629 | name = "tokio-stream" 630 | version = "0.1.17" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" 633 | dependencies = [ 634 | "futures-core", 635 | "pin-project-lite", 636 | "tokio", 637 | "tokio-util", 638 | ] 639 | 640 | [[package]] 641 | name = "tokio-util" 642 | version = "0.7.14" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 645 | dependencies = [ 646 | "bytes", 647 | "futures-core", 648 | "futures-sink", 649 | "pin-project-lite", 650 | "tokio", 651 | ] 652 | 653 | [[package]] 654 | name = "tracing" 655 | version = "0.1.41" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 658 | dependencies = [ 659 | "pin-project-lite", 660 | "tracing-core", 661 | ] 662 | 663 | [[package]] 664 | name = "tracing-core" 665 | version = "0.1.33" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 668 | dependencies = [ 669 | "once_cell", 670 | ] 671 | 672 | [[package]] 673 | name = "tracing-subscriber" 674 | version = "0.3.19" 675 | source = "registry+https://github.com/rust-lang/crates.io-index" 676 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 677 | dependencies = [ 678 | "matchers", 679 | "nu-ansi-term", 680 | "once_cell", 681 | "regex", 682 | "sharded-slab", 683 | "thread_local", 684 | "tracing", 685 | "tracing-core", 686 | ] 687 | 688 | [[package]] 689 | name = "unicode-ident" 690 | version = "1.0.17" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 693 | 694 | [[package]] 695 | name = "utf8parse" 696 | version = "0.2.2" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 699 | 700 | [[package]] 701 | name = "wasi" 702 | version = "0.11.0+wasi-snapshot-preview1" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 705 | 706 | [[package]] 707 | name = "winapi" 708 | version = "0.3.9" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 711 | dependencies = [ 712 | "winapi-i686-pc-windows-gnu", 713 | "winapi-x86_64-pc-windows-gnu", 714 | ] 715 | 716 | [[package]] 717 | name = "winapi-i686-pc-windows-gnu" 718 | version = "0.4.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 721 | 722 | [[package]] 723 | name = "winapi-x86_64-pc-windows-gnu" 724 | version = "0.4.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 727 | 728 | [[package]] 729 | name = "windows-sys" 730 | version = "0.52.0" 731 | source = "registry+https://github.com/rust-lang/crates.io-index" 732 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 733 | dependencies = [ 734 | "windows-targets", 735 | ] 736 | 737 | [[package]] 738 | name = "windows-sys" 739 | version = "0.59.0" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 742 | dependencies = [ 743 | "windows-targets", 744 | ] 745 | 746 | [[package]] 747 | name = "windows-targets" 748 | version = "0.52.6" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 751 | dependencies = [ 752 | "windows_aarch64_gnullvm", 753 | "windows_aarch64_msvc", 754 | "windows_i686_gnu", 755 | "windows_i686_gnullvm", 756 | "windows_i686_msvc", 757 | "windows_x86_64_gnu", 758 | "windows_x86_64_gnullvm", 759 | "windows_x86_64_msvc", 760 | ] 761 | 762 | [[package]] 763 | name = "windows_aarch64_gnullvm" 764 | version = "0.52.6" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 767 | 768 | [[package]] 769 | name = "windows_aarch64_msvc" 770 | version = "0.52.6" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 773 | 774 | [[package]] 775 | name = "windows_i686_gnu" 776 | version = "0.52.6" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 779 | 780 | [[package]] 781 | name = "windows_i686_gnullvm" 782 | version = "0.52.6" 783 | source = "registry+https://github.com/rust-lang/crates.io-index" 784 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 785 | 786 | [[package]] 787 | name = "windows_i686_msvc" 788 | version = "0.52.6" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 791 | 792 | [[package]] 793 | name = "windows_x86_64_gnu" 794 | version = "0.52.6" 795 | source = "registry+https://github.com/rust-lang/crates.io-index" 796 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 797 | 798 | [[package]] 799 | name = "windows_x86_64_gnullvm" 800 | version = "0.52.6" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 803 | 804 | [[package]] 805 | name = "windows_x86_64_msvc" 806 | version = "0.52.6" 807 | source = "registry+https://github.com/rust-lang/crates.io-index" 808 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 809 | 810 | [[package]] 811 | name = "zlink" 812 | version = "0.0.1-alpha.1" 813 | dependencies = [ 814 | "futures-util", 815 | "serde", 816 | "serde_repr", 817 | "test-log", 818 | "tokio", 819 | "zlink-tokio", 820 | ] 821 | 822 | [[package]] 823 | name = "zlink-codegen" 824 | version = "0.0.1-alpha.1" 825 | 826 | [[package]] 827 | name = "zlink-core" 828 | version = "0.0.1-alpha.1" 829 | dependencies = [ 830 | "defmt", 831 | "futures-util", 832 | "mayheap", 833 | "memchr", 834 | "pin-project-lite", 835 | "serde", 836 | "serde-json-core", 837 | "serde_json", 838 | "tokio", 839 | "tracing", 840 | ] 841 | 842 | [[package]] 843 | name = "zlink-macros" 844 | version = "0.0.1-alpha.1" 845 | 846 | [[package]] 847 | name = "zlink-micro" 848 | version = "0.0.1-alpha.1" 849 | 850 | [[package]] 851 | name = "zlink-tokio" 852 | version = "0.0.1-alpha.1" 853 | dependencies = [ 854 | "futures-util", 855 | "serde", 856 | "serde_repr", 857 | "test-log", 858 | "tokio", 859 | "tokio-stream", 860 | "zlink-core", 861 | ] 862 | 863 | [[package]] 864 | name = "zlink-usb" 865 | version = "0.0.1-alpha.1" 866 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "zlink", 4 | "zlink-core", 5 | "zlink-codegen", 6 | "zlink-tokio", 7 | "zlink-macros", 8 | "zlink-micro", 9 | "zlink-usb", 10 | ] 11 | resolver = "2" 12 | 13 | [workspace.package] 14 | edition = "2021" 15 | rust-version = "1.82" 16 | license = "MIT" 17 | repository = "https://github.com/zeenix/zlink/" 18 | 19 | [profile.bench] 20 | debug = true 21 | strip = "none" 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zeeshan Ali Khan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zlink 2 | 3 | An asynchronous no-std-compatible Varlink Rust crate. It consists for the following subcrates: 4 | 5 | * `zlink-core`: A no-std and no-alloc crate that provides all the core API. It leaves the actual 6 | transport & high-level API to other crates. This crate is not intended to be used directly. 7 | * `zlink`: The main crate that provides a unified API and will typically be the crate you'd use 8 | directly. It re-exports API from the appropriate crate(s) depending on the cargo feature(s) 9 | enabled. This crate also provides high-level macros to each write clients and services. 10 | * `zlink-tokio`: `tokio`-based transport implementations and runtime integration. 11 | * `zlink-usb` & `zlink-micro`: Together these enables RPC between a (Linux) host and 12 | microcontrollers through USB. The former is targetted for the the host side and latter for the 13 | microcontrollers side. 14 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * zlink-core 4 | * IDL 5 | * `idl` mod 6 | * Type that describes a type: Interface, Method, Type, Error 7 | * Trait that gives the IDL name of the type 8 | * impl for common types 9 | * `Service` 10 | * `Info` type with fields of `GetInfo` method 11 | * impl `GetInfo` method for test case 12 | * cargo features to allow use of `idl` only 13 | * IntrospectionProxy 14 | * zlink-macros 15 | * Provide introspection derives 16 | * Update `Service` example/test to make use of these 17 | * zlink-macros 18 | * `proxy` attribute macro (wraps `Proxy`) 19 | * gated behind (default)`proxy` feature 20 | * `service` attribute macro (see below) 21 | * gated behind `service` feature 22 | * See if we can instead use a macro_rules macro (see for inspiration) 23 | * implements `Service` trait 24 | * handle multiple replies (not covered in the snippet yet) 25 | * introspection 26 | * Add required API to `Service` trait first 27 | * tests 28 | * Update Service docs: Prefer using `service` macro over a manual implementation. 29 | * zlink-codegen (generates code from IDL) 30 | * Make use of `zlink_core::idl` module 31 | * zlink-usb 32 | * USB (using nusb) transport 33 | * zlink-micro 34 | * embassy_usb-based transport 35 | * Driver impl needs to be provided by the user (e.g `embassy-usb-synopsys-otg` for STM32). 36 | * Will need to create a connection concept through multiplexing 37 | * 38 | * Ensure cancelation safety (if needed by Server/Service) is satisfied 39 | * zlink-macros 40 | * `proxy` pipelining 41 | * generate separate send/receive methods for each method in the service 42 | * embedded feature 43 | * Manual Deserialize impl 44 | * assume fields in a specific order 45 | * alloc/std feature (default) 46 | * Make alloc feature of serde optional 47 | * More metadata in Cargo.toml files 48 | 49 | * zlink-core 50 | * FDs 51 | * Graceful shutdown 52 | * More efficient parsing of messages in Connection using winnow 53 | * 54 | * Remove the FIXMEs 55 | * enums support in serde-json-core: 56 | * zlink-smol 57 | * zlink-tokio 58 | * notified 59 | * Send out last message on drop 60 | * builder-pattern setter method to disable this. 61 | * Split Stream so that we don't require Clone for `Once` 62 | 63 | --------------------------------------- 64 | 65 | ## Code snippets 66 | 67 | ### service macro 68 | 69 | ```rust 70 | struct Ftl { 71 | drive_condition: DriveCondition, 72 | coordinates: Coordinate, 73 | } 74 | 75 | // This attribute macro defines a varlink service that can be passed to `Server::run`. 76 | // 77 | // It supports the folowing sub-attributes: 78 | // * `interface`: The interface name. If this is given than all the methods will be prefixed 79 | // with the interface name. This is useful when the service only offers a single interface. 80 | #[zlink_tokio::service] 81 | impl Ftl { 82 | #[zlink(interface = "org.varlink.service.ftl")] 83 | async fn monitor(&mut self) -> Result { 84 | Ok(self.drive_condition) 85 | } 86 | } 87 | 88 | #[derive(Debug, Serialize, Deserialize)] 89 | struct DriveCondition { 90 | state: DriveState, 91 | tylium_level: i64, 92 | } 93 | 94 | #[derive(Debug, Serialize, Deserialize)] 95 | #[serde(rename_all = "snake_case")] 96 | pub enum DriveState { 97 | Idle, 98 | Spooling, 99 | Busy, 100 | } 101 | 102 | #[derive(Debug, Serialize, Deserialize)] 103 | struct DriveConfiguration { 104 | speed: i64, 105 | trajectory: i64, 106 | duration: i64, 107 | } 108 | 109 | #[derive(Debug, Serialize, Deserialize)] 110 | struct Coordinate { 111 | longitude: f32, 112 | latitude: f32, 113 | distance: i64, 114 | } 115 | ``` 116 | -------------------------------------------------------------------------------- /zlink-codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-codegen" 3 | version = "0.0.1-alpha.1" 4 | description = "Utility to generate zlink code from varlink IDL files" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /zlink-codegen/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-codegen/src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_debug_implementations, nonstandard_style, rust_2018_idioms)] 2 | #![warn(unreachable_pub)] 3 | #![doc = include_str!("../README.md")] 4 | 5 | fn main() { 6 | println!("Hello, world!"); 7 | } 8 | -------------------------------------------------------------------------------- /zlink-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-core" 3 | version = "0.0.1-alpha.1" 4 | description = "The core crate of the zlink project" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [features] 11 | default = ["std", "io-buffer-2kb"] 12 | std = ["dep:serde_json", "memchr/std", "mayheap/alloc", "serde/std", "tracing"] 13 | embedded = ["dep:serde-json-core", "mayheap/heapless", "defmt"] 14 | # I/O buffer sizes: 2kb (default), 4kb, 16kb, 64kb, 1mb (highest selected if multiple enabled). 15 | io-buffer-2kb = [] 16 | io-buffer-4kb = [] 17 | io-buffer-16kb = [] 18 | io-buffer-1mb = [] 19 | 20 | [dependencies] 21 | serde = { version = "1.0.218", default-features = false, features = ["derive"] } 22 | serde_json = { version = "1.0.139", optional = true } 23 | serde-json-core = { version = "0.6.0", default-features = false, features = [ 24 | "heapless", 25 | ], optional = true } 26 | mayheap = { git = "https://github.com/zeenix/mayheap", version = "0.2.0", features = [ 27 | "serde", 28 | ], default-features = false } 29 | memchr = { version = "2.7.4", default-features = false } 30 | futures-util = { version = "0.3.31", default-features = false, features = [ 31 | "async-await", 32 | "async-await-macro", 33 | ] } 34 | tracing = { version = "0.1.41", default-features = false, optional = true } 35 | defmt = { version = "1.0.1", default-features = false, optional = true } 36 | pin-project-lite = "0.2.16" 37 | 38 | [dev-dependencies] 39 | serde = { version = "1.0.218", default-features = false, features = ["alloc"] } 40 | tokio = { version = "1.44.0", features = [ 41 | "macros", 42 | "rt", 43 | "rt-multi-thread", 44 | "test-util", 45 | "fs", 46 | ] } 47 | -------------------------------------------------------------------------------- /zlink-core/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-core/src/call.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A method call. 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct Call { 8 | #[serde(flatten)] 9 | pub(super) method: M, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub(super) oneway: Option, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub(super) more: Option, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub(super) upgrade: Option, 16 | } 17 | 18 | impl Call { 19 | /// Create a new method call. 20 | pub fn new(method: M) -> Self { 21 | Self { 22 | method, 23 | oneway: None, 24 | more: None, 25 | upgrade: None, 26 | } 27 | } 28 | 29 | /// Set the oneway flag. 30 | pub fn set_oneway(mut self, oneway: Option) -> Self { 31 | self.oneway = oneway; 32 | self 33 | } 34 | 35 | /// Set the more flag. 36 | pub fn set_more(mut self, more: Option) -> Self { 37 | self.more = more; 38 | self 39 | } 40 | 41 | /// Set the upgrade flag. 42 | pub fn set_upgrade(mut self, upgrade: Option) -> Self { 43 | self.upgrade = upgrade; 44 | self 45 | } 46 | 47 | /// The method call name and parameters. 48 | pub fn method(&self) -> &M { 49 | &self.method 50 | } 51 | 52 | /// If the method call doesn't want a reply. 53 | pub fn oneway(&self) -> Option { 54 | self.oneway 55 | } 56 | 57 | /// If the method call is requesting more replies. 58 | pub fn more(&self) -> Option { 59 | self.more 60 | } 61 | 62 | /// If the method call is requesting an upgrade to a different protocol. 63 | pub fn upgrade(&self) -> Option { 64 | self.upgrade 65 | } 66 | } 67 | 68 | impl From for Call { 69 | fn from(method: M) -> Self { 70 | Self::new(method) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /zlink-core/src/connection/chain/mod.rs: -------------------------------------------------------------------------------- 1 | //! Chain method calls. 2 | 3 | mod reply_stream; 4 | 5 | use crate::{connection::Socket, reply, Call, Connection, Result}; 6 | use core::fmt::Debug; 7 | use futures_util::stream::Stream; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use reply_stream::ReplyStream; 11 | 12 | /// A chain of method calls that will be sent together. 13 | /// 14 | /// Each call must have the same method, reply, and error types for homogeneity. Use 15 | /// [`Connection::chain_call`] to create a new chain, extend it with [`Chain::append`] and send the 16 | /// the entire chain using [`Chain::send`]. 17 | /// 18 | /// With `std` feature enabled, this supports unlimited calls. Otherwise it is limited by how many 19 | /// calls can fit in our fixed-sized buffer. 20 | /// 21 | /// Oneway calls (where `Call::oneway() == Some(true)`) do not expect replies and are handled 22 | /// automatically by the chain. 23 | #[derive(Debug)] 24 | pub struct Chain<'c, S: Socket, Method, Params, ReplyError> { 25 | pub(super) connection: &'c mut Connection, 26 | pub(super) call_count: usize, 27 | pub(super) reply_count: usize, 28 | _phantom: core::marker::PhantomData<(Method, Params, ReplyError)>, 29 | } 30 | 31 | impl<'c, S, Method, Params, ReplyError> Chain<'c, S, Method, Params, ReplyError> 32 | where 33 | S: Socket, 34 | Method: Serialize + Debug, 35 | Params: Deserialize<'c> + Debug, 36 | ReplyError: Deserialize<'c> + Debug, 37 | { 38 | /// Create a new chain with the first call. 39 | pub(super) fn new(connection: &'c mut Connection, call: &Call) -> Result { 40 | connection.write.enqueue_call(call)?; 41 | let reply_count = if call.oneway() == Some(true) { 0 } else { 1 }; 42 | Ok(Chain { 43 | connection, 44 | call_count: 1, 45 | reply_count, 46 | _phantom: core::marker::PhantomData, 47 | }) 48 | } 49 | 50 | /// Append another method call to the chain. 51 | /// 52 | /// The call will be enqueued but not sent until [`Chain::send`] is called. Note that one way 53 | /// calls (where `Call::oneway() == Some(true)`) do not receive replies. 54 | /// 55 | /// Calls with `more == Some(true)` will stream multiple replies until a reply with 56 | /// `continues != Some(true)` is received. 57 | pub fn append(mut self, call: &Call) -> Result { 58 | self.connection.write.enqueue_call(call)?; 59 | if call.oneway() != Some(true) { 60 | self.reply_count += 1; 61 | }; 62 | self.call_count += 1; 63 | Ok(self) 64 | } 65 | 66 | /// Send all enqueued calls and return a replies stream. 67 | /// 68 | /// This will flush all enqueued calls in a single write operation and then return a stream 69 | /// that allows reading the replies. 70 | pub async fn send( 71 | self, 72 | ) -> Result>> + 'c> 73 | where 74 | Params: 'c, 75 | ReplyError: 'c, 76 | { 77 | // Flush all enqueued calls. 78 | self.connection.write.flush().await?; 79 | 80 | Ok(ReplyStream::new( 81 | self.connection.read_mut(), 82 | |conn| conn.receive_reply::(), 83 | self.reply_count, 84 | )) 85 | } 86 | } 87 | 88 | #[cfg(test)] 89 | mod tests { 90 | use super::*; 91 | use crate::{ 92 | connection::socket::{ReadHalf, Socket, WriteHalf}, 93 | Call, 94 | }; 95 | use futures_util::pin_mut; 96 | use mayheap::Vec; 97 | use serde::{Deserialize, Serialize}; 98 | 99 | #[derive(Debug, Serialize, Deserialize)] 100 | struct GetUser { 101 | id: u32, 102 | } 103 | 104 | #[derive(Debug, Serialize, Deserialize)] 105 | struct User { 106 | id: u32, 107 | } 108 | 109 | #[derive(Debug, Serialize, Deserialize)] 110 | struct ApiError { 111 | code: i32, 112 | } 113 | 114 | // Types for heterogeneous calls test 115 | #[derive(Debug, Serialize, Deserialize)] 116 | #[serde(tag = "method")] 117 | enum HeterogeneousMethods { 118 | GetUser { id: u32 }, 119 | GetPost { post_id: u32 }, 120 | DeleteUser { user_id: u32 }, 121 | } 122 | 123 | #[derive(Debug, Serialize, Deserialize)] 124 | struct Post { 125 | id: u32, 126 | title: mayheap::String<32>, 127 | } 128 | 129 | #[derive(Debug, Serialize, Deserialize)] 130 | struct DeleteResult { 131 | success: bool, 132 | } 133 | 134 | #[derive(Debug, Serialize, Deserialize)] 135 | #[serde(untagged)] 136 | enum HeterogeneousResponses { 137 | Post(Post), 138 | User(User), 139 | DeleteResult(DeleteResult), 140 | } 141 | 142 | #[derive(Debug, Serialize, Deserialize)] 143 | struct PostError { 144 | message: mayheap::String<64>, 145 | } 146 | 147 | #[derive(Debug, Serialize, Deserialize)] 148 | struct DeleteError { 149 | reason: mayheap::String<64>, 150 | } 151 | 152 | #[derive(Debug, Serialize, Deserialize)] 153 | #[serde(untagged)] 154 | enum HeterogeneousErrors { 155 | UserError(ApiError), 156 | PostError(PostError), 157 | DeleteError(DeleteError), 158 | } 159 | 160 | // Mock socket implementation for testing. 161 | #[derive(Debug)] 162 | struct MockSocket { 163 | read_data: Vec, 164 | read_pos: usize, 165 | } 166 | 167 | impl MockSocket { 168 | fn new(responses: &[&str]) -> Self { 169 | let mut data = Vec::new(); 170 | 171 | for response in responses { 172 | data.extend_from_slice(response.as_bytes()).unwrap(); 173 | data.push(b'\0').unwrap(); 174 | } 175 | // Add an extra null byte to mark end of all messages 176 | data.push(b'\0').unwrap(); 177 | 178 | Self { 179 | read_data: data, 180 | read_pos: 0, 181 | } 182 | } 183 | } 184 | 185 | impl Socket for MockSocket { 186 | type ReadHalf = MockReadHalf; 187 | type WriteHalf = MockWriteHalf; 188 | 189 | fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { 190 | ( 191 | MockReadHalf { 192 | data: self.read_data, 193 | pos: self.read_pos, 194 | }, 195 | MockWriteHalf { 196 | written: Vec::new(), 197 | }, 198 | ) 199 | } 200 | } 201 | 202 | #[derive(Debug)] 203 | struct MockReadHalf { 204 | data: Vec, 205 | pos: usize, 206 | } 207 | 208 | impl ReadHalf for MockReadHalf { 209 | async fn read(&mut self, buf: &mut [u8]) -> crate::Result { 210 | let remaining = self.data.len().saturating_sub(self.pos); 211 | if remaining == 0 { 212 | return Ok(0); 213 | } 214 | 215 | let to_read = remaining.min(buf.len()); 216 | buf[..to_read].copy_from_slice(&self.data[self.pos..self.pos + to_read]); 217 | self.pos += to_read; 218 | Ok(to_read) 219 | } 220 | } 221 | 222 | #[derive(Debug)] 223 | struct MockWriteHalf { 224 | written: Vec, 225 | } 226 | 227 | impl WriteHalf for MockWriteHalf { 228 | async fn write(&mut self, buf: &[u8]) -> crate::Result<()> { 229 | self.written.extend_from_slice(buf).unwrap(); 230 | Ok(()) 231 | } 232 | } 233 | 234 | #[tokio::test] 235 | async fn homogeneous_calls() -> crate::Result<()> { 236 | let responses = [r#"{"parameters":{"id":1}}"#, r#"{"parameters":{"id":2}}"#]; 237 | let socket = MockSocket::new(&responses); 238 | let mut conn = Connection::new(socket); 239 | 240 | let call1 = Call::new(GetUser { id: 1 }); 241 | let call2 = Call::new(GetUser { id: 2 }); 242 | 243 | let replies = conn 244 | .chain_call::(&call1)? 245 | .append(&call2)? 246 | .send() 247 | .await?; 248 | 249 | use futures_util::stream::StreamExt; 250 | pin_mut!(replies); 251 | 252 | let user1 = replies.next().await.unwrap()?.unwrap(); 253 | assert_eq!(user1.parameters().unwrap().id, 1); 254 | 255 | let user2 = replies.next().await.unwrap()?.unwrap(); 256 | assert_eq!(user2.parameters().unwrap().id, 2); 257 | 258 | // No more replies should be available. 259 | let no_reply = replies.next().await; 260 | assert!(no_reply.is_none()); 261 | Ok(()) 262 | } 263 | 264 | #[tokio::test] 265 | async fn oneway_calls_no_reply() -> crate::Result<()> { 266 | // Only the first call expects a reply; the second is oneway. 267 | let responses = [r#"{"parameters":{"id":1}}"#]; 268 | let socket = MockSocket::new(&responses); 269 | let mut conn = Connection::new(socket); 270 | 271 | let get_user = Call::new(GetUser { id: 1 }); 272 | let oneway_call = Call::new(GetUser { id: 2 }).set_oneway(Some(true)); 273 | 274 | let replies = conn 275 | .chain_call::(&get_user)? 276 | .append(&oneway_call)? 277 | .send() 278 | .await?; 279 | 280 | use futures_util::stream::StreamExt; 281 | pin_mut!(replies); 282 | 283 | let user = replies.next().await.unwrap()?.unwrap(); 284 | assert_eq!(user.parameters().unwrap().id, 1); 285 | 286 | // No more replies should be available. 287 | let no_reply = replies.next().await; 288 | assert!(no_reply.is_none()); 289 | Ok(()) 290 | } 291 | 292 | #[tokio::test] 293 | async fn more_calls_with_streaming() -> crate::Result<()> { 294 | let responses = [ 295 | r#"{"parameters":{"id":1},"continues":true}"#, 296 | r#"{"parameters":{"id":2},"continues":true}"#, 297 | r#"{"parameters":{"id":3},"continues":false}"#, 298 | r#"{"parameters":{"id":4}}"#, 299 | ]; 300 | let socket = MockSocket::new(&responses); 301 | let mut conn = Connection::new(socket); 302 | 303 | let more_call = Call::new(GetUser { id: 1 }).set_more(Some(true)); 304 | let regular_call = Call::new(GetUser { id: 2 }); 305 | 306 | let replies = conn 307 | .chain_call::(&more_call)? 308 | .append(®ular_call)? 309 | .send() 310 | .await?; 311 | 312 | use futures_util::stream::StreamExt; 313 | pin_mut!(replies); 314 | 315 | // First call - streaming replies 316 | let user1 = replies.next().await.unwrap()?.unwrap(); 317 | assert_eq!(user1.parameters().unwrap().id, 1); 318 | assert_eq!(user1.continues(), Some(true)); 319 | 320 | let user2 = replies.next().await.unwrap()?.unwrap(); 321 | assert_eq!(user2.parameters().unwrap().id, 2); 322 | assert_eq!(user2.continues(), Some(true)); 323 | 324 | let user3 = replies.next().await.unwrap()?.unwrap(); 325 | assert_eq!(user3.parameters().unwrap().id, 3); 326 | assert_eq!(user3.continues(), Some(false)); 327 | 328 | // Second call - single reply 329 | let user4 = replies.next().await.unwrap()?.unwrap(); 330 | assert_eq!(user4.parameters().unwrap().id, 4); 331 | assert_eq!(user4.continues(), None); 332 | 333 | // No more replies should be available. 334 | let no_reply = replies.next().await; 335 | assert!(no_reply.is_none()); 336 | Ok(()) 337 | } 338 | 339 | #[tokio::test] 340 | async fn stream_interface_works() -> crate::Result<()> { 341 | use futures_util::stream::StreamExt; 342 | 343 | let responses = [ 344 | r#"{"parameters":{"id":1}}"#, 345 | r#"{"parameters":{"id":2}}"#, 346 | r#"{"parameters":{"id":3}}"#, 347 | ]; 348 | let socket = MockSocket::new(&responses); 349 | let mut conn = Connection::new(socket); 350 | 351 | let call1 = Call::new(GetUser { id: 1 }); 352 | let call2 = Call::new(GetUser { id: 2 }); 353 | let call3 = Call::new(GetUser { id: 3 }); 354 | 355 | let replies = conn 356 | .chain_call::(&call1)? 357 | .append(&call2)? 358 | .append(&call3)? 359 | .send() 360 | .await?; 361 | 362 | // Use Stream's collect method to gather all results 363 | pin_mut!(replies); 364 | let results: mayheap::Vec<_, 16> = replies.collect().await; 365 | assert_eq!(results.len(), 3); 366 | 367 | // Verify all results are successful 368 | for (i, result) in results.into_iter().enumerate() { 369 | let user = result?.unwrap(); 370 | assert_eq!(user.parameters().unwrap().id, (i + 1) as u32); 371 | } 372 | 373 | Ok(()) 374 | } 375 | 376 | #[cfg(feature = "std")] 377 | #[tokio::test] 378 | async fn heterogeneous_calls() -> crate::Result<()> { 379 | let responses = [ 380 | r#"{"parameters":{"id":1}}"#, 381 | r#"{"parameters":{"id":123,"title":"Test Post"}}"#, 382 | r#"{"parameters":{"success":true}}"#, 383 | ]; 384 | let socket = MockSocket::new(&responses); 385 | let mut conn = Connection::new(socket); 386 | 387 | let get_user_call = Call::new(HeterogeneousMethods::GetUser { id: 1 }); 388 | let get_post_call = Call::new(HeterogeneousMethods::GetPost { post_id: 123 }); 389 | let delete_user_call = Call::new(HeterogeneousMethods::DeleteUser { user_id: 456 }); 390 | 391 | let replies = conn 392 | .chain_call::( 393 | &get_user_call, 394 | )? 395 | .append(&get_post_call)? 396 | .append(&delete_user_call)? 397 | .send() 398 | .await?; 399 | 400 | use futures_util::stream::StreamExt; 401 | pin_mut!(replies); 402 | 403 | // First response: User 404 | let user_response = replies.next().await.unwrap()?.unwrap(); 405 | if let HeterogeneousResponses::User(user) = user_response.parameters().unwrap() { 406 | assert_eq!(user.id, 1); 407 | } else { 408 | panic!("Expected User response"); 409 | } 410 | 411 | // Second response: Post 412 | let post_response = replies.next().await.unwrap()?.unwrap(); 413 | if let HeterogeneousResponses::Post(post) = post_response.parameters().unwrap() { 414 | assert_eq!(post.id, 123); 415 | assert_eq!(post.title, "Test Post"); 416 | } else { 417 | panic!("Expected Post response"); 418 | } 419 | 420 | // Third response: DeleteResult 421 | let delete_response = replies.next().await.unwrap()?.unwrap(); 422 | if let HeterogeneousResponses::DeleteResult(result) = delete_response.parameters().unwrap() 423 | { 424 | assert_eq!(result.success, true); 425 | } else { 426 | panic!("Expected DeleteResult response"); 427 | } 428 | 429 | // No more replies should be available. 430 | let no_reply = replies.next().await; 431 | assert!(no_reply.is_none()); 432 | Ok(()) 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /zlink-core/src/connection/chain/reply_stream.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | fmt::Debug, 3 | future::Future, 4 | pin::Pin, 5 | task::{ready, Context, Poll}, 6 | }; 7 | use futures_util::stream::Stream; 8 | use pin_project_lite::pin_project; 9 | use serde::Deserialize; 10 | 11 | use crate::{ 12 | connection::{socket::ReadHalf, ReadConnection}, 13 | reply, Result, 14 | }; 15 | 16 | pin_project! { 17 | /// A stream of replies from a chain of method calls. 18 | #[derive(Debug)] 19 | pub(super) struct ReplyStream<'c, Read: ReadHalf, F, Fut, Params, ReplyError> { 20 | #[pin] 21 | state: ReplyStreamState, 22 | connection: &'c mut ReadConnection, 23 | func: F, 24 | call_count: usize, 25 | current_index: usize, 26 | done: bool, 27 | _phantom: core::marker::PhantomData<(Params, ReplyError)>, 28 | } 29 | } 30 | 31 | impl<'c, Read, F, Fut, Params, ReplyError> ReplyStream<'c, Read, F, Fut, Params, ReplyError> 32 | where 33 | Read: ReadHalf, 34 | F: FnMut(&'c mut ReadConnection) -> Fut, 35 | Fut: Future>>, 36 | Params: Deserialize<'c> + Debug, 37 | ReplyError: Deserialize<'c> + Debug, 38 | { 39 | pub(super) fn new( 40 | connection: &'c mut ReadConnection, 41 | func: F, 42 | call_count: usize, 43 | ) -> Self { 44 | ReplyStream { 45 | state: ReplyStreamState::Init, 46 | connection, 47 | func, 48 | call_count, 49 | current_index: 0, 50 | done: false, 51 | _phantom: core::marker::PhantomData, 52 | } 53 | } 54 | } 55 | 56 | impl<'c, Read, F, Fut, Params, ReplyError> Stream 57 | for ReplyStream<'c, Read, F, Fut, Params, ReplyError> 58 | where 59 | Read: ReadHalf, 60 | F: FnMut(&'c mut ReadConnection) -> Fut, 61 | Fut: Future>>, 62 | Params: Deserialize<'c> + Debug, 63 | ReplyError: Deserialize<'c> + Debug, 64 | { 65 | type Item = Result>; 66 | 67 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 68 | let mut this = self.project(); 69 | if *this.done { 70 | return Poll::Ready(None); 71 | } 72 | 73 | if this.state.as_mut().check_init() { 74 | let conn = unsafe { &mut *(*this.connection as *mut _) }; 75 | this.state.set(ReplyStreamState::Future { 76 | future: (this.func)(conn), 77 | }); 78 | } 79 | 80 | let item = match this.state.as_mut().project_future() { 81 | Some(fut) => ready!(fut.poll(cx)), 82 | None => panic!("ReplyStream must not be polled after it returned `Poll::Ready(None)`"), 83 | }; 84 | 85 | // Only increment current_index if this is the last reply for this call. 86 | // (i.e., continues is not Some(true)) 87 | match &item { 88 | Ok(Ok(reply)) if reply.continues() != Some(true) => { 89 | *this.current_index += 1; 90 | } 91 | Ok(Ok(_)) => { 92 | // Streaming reply, don't increment index yet. 93 | } 94 | Ok(Err(_)) => { 95 | // For method errors, always increment since there won't be more replies. 96 | *this.current_index += 1; 97 | } 98 | Err(_) => { 99 | // If there was a general error, mark the stream as done as it's likely not 100 | // recoverable. 101 | *this.done = true; 102 | } 103 | } 104 | if *this.current_index >= *this.call_count { 105 | *this.done = true; 106 | } 107 | 108 | this.state.set(ReplyStreamState::Init); 109 | 110 | Poll::Ready(Some(item)) 111 | } 112 | } 113 | 114 | pin_project! { 115 | /// State for `ReplyStream`. 116 | /// 117 | /// Based on the [`futures::stream::unfold`] implementation. 118 | #[project = ReplyStreamStateProj] 119 | #[project_replace = ReplyStreamStateProjReplace] 120 | #[derive(Debug)] 121 | enum ReplyStreamState { 122 | Init, 123 | Future { 124 | #[pin] 125 | future: R, 126 | }, 127 | Empty, 128 | } 129 | } 130 | 131 | impl ReplyStreamState { 132 | fn project_future(self: Pin<&mut Self>) -> Option> { 133 | match self.project() { 134 | ReplyStreamStateProj::Future { future } => Some(future), 135 | _ => None, 136 | } 137 | } 138 | 139 | fn check_init(self: Pin<&mut Self>) -> bool { 140 | match &*self { 141 | Self::Init => match self.project_replace(Self::Empty) { 142 | ReplyStreamStateProjReplace::Init => true, 143 | _ => unreachable!(), 144 | }, 145 | _ => false, 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /zlink-core/src/connection/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains connection related API. 2 | 3 | mod read_connection; 4 | pub use read_connection::ReadConnection; 5 | pub mod chain; 6 | pub mod socket; 7 | mod write_connection; 8 | use crate::{ 9 | reply::{self, Reply}, 10 | Call, Result, 11 | }; 12 | pub use chain::Chain; 13 | use core::{fmt::Debug, sync::atomic::AtomicUsize}; 14 | pub use write_connection::WriteConnection; 15 | 16 | use serde::{Deserialize, Serialize}; 17 | pub use socket::Socket; 18 | 19 | /// A connection. 20 | /// 21 | /// The low-level API to send and receive messages. 22 | /// 23 | /// Each connection gets a unique identifier when created that can be queried using 24 | /// [`Connection::id`]. This ID is shared betwen the read and write halves of the connection. It 25 | /// can be used to associate the read and write halves of the same connection. 26 | /// 27 | /// # Cancel safety 28 | /// 29 | /// All async methods of this type are cancel safe unless explicitly stated otherwise in its 30 | /// documentation. 31 | #[derive(Debug)] 32 | pub struct Connection { 33 | read: ReadConnection, 34 | write: WriteConnection, 35 | } 36 | 37 | impl Connection 38 | where 39 | S: Socket, 40 | { 41 | /// Create a new connection. 42 | pub fn new(socket: S) -> Self { 43 | let (read, write) = socket.split(); 44 | let id = NEXT_ID.fetch_add(1, core::sync::atomic::Ordering::Relaxed); 45 | Self { 46 | read: ReadConnection::new(read, id), 47 | write: WriteConnection::new(write, id), 48 | } 49 | } 50 | 51 | /// The reference to the read half of the connection. 52 | pub fn read(&self) -> &ReadConnection { 53 | &self.read 54 | } 55 | 56 | /// The mutable reference to the read half of the connection. 57 | pub fn read_mut(&mut self) -> &mut ReadConnection { 58 | &mut self.read 59 | } 60 | 61 | /// The reference to the write half of the connection. 62 | pub fn write(&self) -> &WriteConnection { 63 | &self.write 64 | } 65 | 66 | /// The mutable reference to the write half of the connection. 67 | pub fn write_mut(&mut self) -> &mut WriteConnection { 68 | &mut self.write 69 | } 70 | 71 | /// Split the connection into read and write halves. 72 | pub fn split(self) -> (ReadConnection, WriteConnection) { 73 | (self.read, self.write) 74 | } 75 | 76 | /// Join the read and write halves into a connection (the opposite of [`Connection::split`]). 77 | pub fn join(read: ReadConnection, write: WriteConnection) -> Self { 78 | Self { read, write } 79 | } 80 | 81 | /// The unique identifier of the connection. 82 | pub fn id(&self) -> usize { 83 | assert_eq!(self.read.id(), self.write.id()); 84 | self.read.id() 85 | } 86 | 87 | /// Sends a method call. 88 | /// 89 | /// Convenience wrapper around [`WriteConnection::send_call`]. 90 | pub async fn send_call(&mut self, call: &Call) -> Result<()> 91 | where 92 | Method: Serialize + Debug, 93 | { 94 | self.write.send_call(call).await 95 | } 96 | 97 | /// Receives a method call reply. 98 | /// 99 | /// Convenience wrapper around [`ReadConnection::receive_reply`]. 100 | pub async fn receive_reply<'r, Params, ReplyError>( 101 | &'r mut self, 102 | ) -> Result> 103 | where 104 | Params: Deserialize<'r> + Debug, 105 | ReplyError: Deserialize<'r> + Debug, 106 | { 107 | self.read.receive_reply().await 108 | } 109 | 110 | /// Call a method and receive a reply. 111 | /// 112 | /// This is a convenience method that combines [`Connection::send_call`] and 113 | /// [`Connection::receive_reply`]. 114 | pub async fn call_method<'r, Method, Params, ReplyError>( 115 | &'r mut self, 116 | call: &Call, 117 | ) -> Result> 118 | where 119 | Method: Serialize + Debug, 120 | Params: Deserialize<'r> + Debug, 121 | ReplyError: Deserialize<'r> + Debug, 122 | { 123 | self.send_call(call).await?; 124 | self.receive_reply().await 125 | } 126 | 127 | /// Receive a method call over the socket. 128 | /// 129 | /// Convenience wrapper around [`ReadConnection::receive_call`]. 130 | pub async fn receive_call<'m, Method>(&'m mut self) -> Result> 131 | where 132 | Method: Deserialize<'m> + Debug, 133 | { 134 | self.read.receive_call().await 135 | } 136 | 137 | /// Send a reply over the socket. 138 | /// 139 | /// Convenience wrapper around [`WriteConnection::send_reply`]. 140 | pub async fn send_reply(&mut self, reply: &Reply) -> Result<()> 141 | where 142 | Params: Serialize + Debug, 143 | { 144 | self.write.send_reply(reply).await 145 | } 146 | 147 | /// Send an error reply over the socket. 148 | /// 149 | /// Convenience wrapper around [`WriteConnection::send_error`]. 150 | pub async fn send_error(&mut self, error: &ReplyError) -> Result<()> 151 | where 152 | ReplyError: Serialize + Debug, 153 | { 154 | self.write.send_error(error).await 155 | } 156 | 157 | /// Enqueue a call to the server. 158 | /// 159 | /// Convenience wrapper around [`WriteConnection::enqueue_call`]. 160 | pub fn enqueue_call(&mut self, method: &Call) -> Result<()> 161 | where 162 | Method: Serialize + Debug, 163 | { 164 | self.write.enqueue_call(method) 165 | } 166 | 167 | /// Flush the connection. 168 | /// 169 | /// Convenience wrapper around [`WriteConnection::flush`]. 170 | pub async fn flush(&mut self) -> Result<()> { 171 | self.write.flush().await 172 | } 173 | 174 | /// Start a chain of method calls. 175 | /// 176 | /// This allows batching multiple calls together and sending them in a single write operation. 177 | /// 178 | /// # Examples 179 | /// 180 | /// ## Basic Usage with Sequential Access 181 | /// 182 | /// ```no_run 183 | /// use zlink_core::{Connection, Call, reply}; 184 | /// use serde::{Serialize, Deserialize}; 185 | /// use futures_util::{pin_mut, stream::StreamExt}; 186 | /// 187 | /// # async fn example() -> zlink_core::Result<()> { 188 | /// # let mut conn: Connection = todo!(); 189 | /// 190 | /// #[derive(Debug, Serialize, Deserialize)] 191 | /// #[serde(tag = "method", content = "parameters")] 192 | /// enum Methods { 193 | /// #[serde(rename = "org.example.GetUser")] 194 | /// GetUser { id: u32 }, 195 | /// #[serde(rename = "org.example.GetProject")] 196 | /// GetProject { id: u32 }, 197 | /// } 198 | /// 199 | /// #[derive(Debug, Deserialize)] 200 | /// struct User { name: String } 201 | /// 202 | /// #[derive(Debug, Deserialize)] 203 | /// struct Project { title: String } 204 | /// 205 | /// #[derive(Debug, Deserialize)] 206 | /// #[serde(tag = "error", content = "parameters")] 207 | /// enum ApiError { 208 | /// #[serde(rename = "org.example.UserNotFound")] 209 | /// UserNotFound { code: i32 }, 210 | /// #[serde(rename = "org.example.ProjectNotFound")] 211 | /// ProjectNotFound { code: i32 }, 212 | /// } 213 | /// 214 | /// let get_user = Call::new(Methods::GetUser { id: 1 }); 215 | /// let get_project = Call::new(Methods::GetProject { id: 2 }); 216 | /// 217 | /// // Chain calls and send them in a batch 218 | /// let replies = conn 219 | /// .chain_call::(&get_user)? 220 | /// .append(&get_project)? 221 | /// .send().await?; 222 | /// pin_mut!(replies); 223 | /// 224 | /// // Access replies sequentially - types are now fixed by the chain 225 | /// let user_reply = replies.next().await.unwrap()?; 226 | /// let project_reply = replies.next().await.unwrap()?; 227 | /// 228 | /// match user_reply { 229 | /// Ok(user) => println!("User: {}", user.parameters().unwrap().name), 230 | /// Err(error) => println!("User error: {:?}", error), 231 | /// } 232 | /// # Ok(()) 233 | /// # } 234 | /// ``` 235 | /// 236 | /// ## Arbitrary Number of Calls 237 | /// 238 | /// ```no_run 239 | /// # use zlink_core::{Connection, Call, reply}; 240 | /// # use serde::{Serialize, Deserialize}; 241 | /// # use futures_util::{pin_mut, stream::StreamExt}; 242 | /// # async fn example() -> zlink_core::Result<()> { 243 | /// # let mut conn: Connection = todo!(); 244 | /// # #[derive(Debug, Serialize, Deserialize)] 245 | /// # #[serde(tag = "method", content = "parameters")] 246 | /// # enum Methods { 247 | /// # #[serde(rename = "org.example.GetUser")] 248 | /// # GetUser { id: u32 }, 249 | /// # } 250 | /// # #[derive(Debug, Deserialize)] 251 | /// # struct User { name: String } 252 | /// # #[derive(Debug, Deserialize)] 253 | /// # #[serde(tag = "error", content = "parameters")] 254 | /// # enum ApiError { 255 | /// # #[serde(rename = "org.example.UserNotFound")] 256 | /// # UserNotFound { code: i32 }, 257 | /// # #[serde(rename = "org.example.ProjectNotFound")] 258 | /// # ProjectNotFound { code: i32 }, 259 | /// # } 260 | /// # let get_user = Call::new(Methods::GetUser { id: 1 }); 261 | /// 262 | /// // Chain many calls (no upper limit) 263 | /// let mut chain = conn.chain_call::(&get_user)?; 264 | /// for i in 2..100 { 265 | /// chain = chain.append(&Call::new(Methods::GetUser { id: i }))?; 266 | /// } 267 | /// 268 | /// let replies = chain.send().await?; 269 | /// pin_mut!(replies); 270 | /// 271 | /// // Process all replies sequentially - types are fixed by the chain 272 | /// while let Some(user_reply) = replies.next().await { 273 | /// let user_reply = user_reply?; 274 | /// // Handle each reply... 275 | /// match user_reply { 276 | /// Ok(user) => println!("User: {}", user.parameters().unwrap().name), 277 | /// Err(error) => println!("Error: {:?}", error), 278 | /// } 279 | /// } 280 | /// # Ok(()) 281 | /// # } 282 | /// ``` 283 | /// 284 | /// # Performance Benefits 285 | /// 286 | /// Instead of multiple write operations, the chain sends all calls in a single 287 | /// write operation, reducing context switching and therefore minimizing latency. 288 | pub fn chain_call<'c, Method, Params, ReplyError>( 289 | &'c mut self, 290 | call: &Call, 291 | ) -> Result> 292 | where 293 | Method: Serialize + Debug, 294 | Params: Deserialize<'c> + Debug, 295 | ReplyError: Deserialize<'c> + Debug, 296 | { 297 | Chain::new(self, call) 298 | } 299 | } 300 | 301 | impl From for Connection 302 | where 303 | S: Socket, 304 | { 305 | fn from(socket: S) -> Self { 306 | Self::new(socket) 307 | } 308 | } 309 | 310 | #[cfg(feature = "io-buffer-1mb")] 311 | pub(crate) const BUFFER_SIZE: usize = 1024 * 1024; 312 | #[cfg(all(not(feature = "io-buffer-1mb"), feature = "io-buffer-16kb"))] 313 | pub(crate) const BUFFER_SIZE: usize = 16 * 1024; 314 | #[cfg(all( 315 | not(feature = "io-buffer-1mb"), 316 | not(feature = "io-buffer-16kb"), 317 | feature = "io-buffer-4kb" 318 | ))] 319 | pub(crate) const BUFFER_SIZE: usize = 4 * 1024; 320 | #[cfg(all( 321 | not(feature = "io-buffer-1mb"), 322 | not(feature = "io-buffer-16kb"), 323 | not(feature = "io-buffer-4kb"), 324 | ))] 325 | pub(crate) const BUFFER_SIZE: usize = 4 * 1024; 326 | 327 | #[cfg(feature = "std")] 328 | const MAX_BUFFER_SIZE: usize = 100 * 1024 * 1024; // Don't allow buffers over 100MB. 329 | 330 | static NEXT_ID: AtomicUsize = AtomicUsize::new(0); 331 | -------------------------------------------------------------------------------- /zlink-core/src/connection/read_connection.rs: -------------------------------------------------------------------------------- 1 | //! Contains connection related API. 2 | 3 | use core::fmt::Debug; 4 | 5 | use crate::Result; 6 | 7 | #[cfg(feature = "std")] 8 | use super::MAX_BUFFER_SIZE; 9 | use super::{ 10 | reply::{self, Reply}, 11 | socket::ReadHalf, 12 | Call, BUFFER_SIZE, 13 | }; 14 | use mayheap::Vec; 15 | use memchr::memchr; 16 | use serde::Deserialize; 17 | 18 | /// A connection that can only be used for reading. 19 | /// 20 | /// # Cancel safety 21 | /// 22 | /// All async methods of this type are cancel safe unless explicitly stated otherwise in its 23 | /// documentation. 24 | #[derive(Debug)] 25 | pub struct ReadConnection { 26 | socket: Read, 27 | read_pos: usize, 28 | msg_pos: usize, 29 | buffer: Vec, 30 | id: usize, 31 | } 32 | 33 | impl ReadConnection { 34 | /// Create a new connection. 35 | pub(super) fn new(socket: Read, id: usize) -> Self { 36 | Self { 37 | socket, 38 | read_pos: 0, 39 | msg_pos: 0, 40 | id, 41 | buffer: Vec::from_slice(&[0; BUFFER_SIZE]).unwrap(), 42 | } 43 | } 44 | 45 | /// The unique identifier of the connection. 46 | #[inline] 47 | pub fn id(&self) -> usize { 48 | self.id 49 | } 50 | 51 | /// Receives a method call reply. 52 | /// 53 | /// The generic parameters needs some explanation: 54 | /// 55 | /// * `Params` is the type of the successful reply. This should be a type that can deserialize 56 | /// itself from the `parameters` field of the reply. 57 | /// * `ReplyError` is the type of the error reply. This should be a type that can deserialize 58 | /// itself from the whole reply object itself and must fail when there is no `error` field in 59 | /// the object. This can be easily achieved using the `serde::Deserialize` derive: 60 | /// 61 | /// ```rust 62 | /// use serde::{Deserialize, Serialize}; 63 | /// 64 | /// #[derive(Debug, Deserialize, Serialize)] 65 | /// #[serde(tag = "error", content = "parameters")] 66 | /// enum MyError { 67 | /// // The name needs to be the fully-qualified name of the error. 68 | /// #[serde(rename = "org.example.ftl.Alpha")] 69 | /// Alpha { param1: u32, param2: String }, 70 | /// #[serde(rename = "org.example.ftl.Bravo")] 71 | /// Bravo, 72 | /// #[serde(rename = "org.example.ftl.Charlie")] 73 | /// Charlie { param1: String }, 74 | /// } 75 | /// ``` 76 | pub async fn receive_reply<'r, Params, ReplyError>( 77 | &'r mut self, 78 | ) -> Result> 79 | where 80 | Params: Deserialize<'r> + Debug, 81 | ReplyError: Deserialize<'r> + Debug, 82 | { 83 | let id = self.id; 84 | let buffer = self.read_message_bytes().await?; 85 | 86 | // First try to parse it as an error. 87 | // FIXME: This will mean the document will be parsed twice. We should instead try to 88 | // quickly check if `error` field is present and then parse to the appropriate type based on 89 | // that information. Perhaps a simple parser using `winnow`? 90 | let ret = match from_slice::(buffer) { 91 | Ok(e) => Ok(Err(e)), 92 | Err(_) => from_slice::>(buffer).map(Ok), 93 | }; 94 | trace!("connection {}: received reply: {:?}", id, ret); 95 | 96 | ret 97 | } 98 | 99 | /// Receive a method call over the socket. 100 | /// 101 | /// The generic `Method` is the type of the method name and its input parameters. This should be 102 | /// a type that can deserialize itself from a complete method call message, i-e an object 103 | /// containing `method` and `parameter` fields. This can be easily achieved using the 104 | /// `serde::Deserialize` derive (See the code snippet in [`super::WriteConnection::send_call`] 105 | /// documentation for an example). 106 | pub async fn receive_call<'m, Method>(&'m mut self) -> Result> 107 | where 108 | Method: Deserialize<'m> + Debug, 109 | { 110 | let id = self.id; 111 | let buffer = self.read_message_bytes().await?; 112 | 113 | let call = from_slice::>(buffer)?; 114 | trace!("connection {}: received a call: {:?}", id, call); 115 | 116 | Ok(call) 117 | } 118 | 119 | // Reads at least one full message from the socket and return a single message bytes. 120 | pub(super) async fn read_message_bytes(&mut self) -> Result<&'_ [u8]> { 121 | self.read_from_socket().await?; 122 | 123 | // Unwrap is safe because `read_from_socket` call above ensures at least one null byte in 124 | // the buffer. 125 | let null_index = memchr(b'\0', &self.buffer[self.msg_pos..]).unwrap() + self.msg_pos; 126 | let buffer = &self.buffer[self.msg_pos..null_index]; 127 | if self.buffer[null_index + 1] == b'\0' { 128 | // This means we're reading the last message and can now reset the indices. 129 | self.read_pos = 0; 130 | self.msg_pos = 0; 131 | } else { 132 | self.msg_pos = null_index + 1; 133 | } 134 | 135 | Ok(buffer) 136 | } 137 | 138 | // Reads at least one full message from the socket. 139 | async fn read_from_socket(&mut self) -> Result<()> { 140 | if self.msg_pos > 0 { 141 | // This means we already have at least one message in the buffer so no need to read. 142 | return Ok(()); 143 | } 144 | 145 | loop { 146 | let bytes_read = self.socket.read(&mut self.buffer[self.read_pos..]).await?; 147 | if bytes_read == 0 { 148 | #[cfg(not(feature = "std"))] 149 | return Err(crate::Error::SocketRead); 150 | #[cfg(feature = "std")] 151 | return Err(crate::Error::Io(std::io::Error::new( 152 | std::io::ErrorKind::UnexpectedEof, 153 | "unexpected EOF", 154 | ))); 155 | } 156 | self.read_pos += bytes_read; 157 | 158 | // This marks end of all messages. After this loop is finished, we'll have 2 consecutive 159 | // null bytes at the end. This is then used by the callers to determine that they've 160 | // read all messages and can now reset the `read_pos`. 161 | self.buffer[self.read_pos] = b'\0'; 162 | 163 | if self.buffer[self.read_pos - 1] == b'\0' { 164 | // One or more full messages were read. 165 | break; 166 | } 167 | 168 | #[cfg(feature = "std")] 169 | if self.read_pos == self.buffer.len() { 170 | if self.read_pos >= MAX_BUFFER_SIZE { 171 | return Err(crate::Error::BufferOverflow); 172 | } 173 | 174 | self.buffer.extend(core::iter::repeat_n(0, BUFFER_SIZE)); 175 | } 176 | } 177 | 178 | Ok(()) 179 | } 180 | } 181 | 182 | fn from_slice<'a, T>(buffer: &'a [u8]) -> Result 183 | where 184 | T: Deserialize<'a>, 185 | { 186 | #[cfg(feature = "std")] 187 | { 188 | serde_json::from_slice::(buffer).map_err(Into::into) 189 | } 190 | 191 | #[cfg(not(feature = "std"))] 192 | { 193 | serde_json_core::from_slice::(buffer) 194 | .map_err(Into::into) 195 | .map(|(e, _)| e) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /zlink-core/src/connection/socket.rs: -------------------------------------------------------------------------------- 1 | //! The low-level Socket read and write traits. 2 | 3 | use core::future::Future; 4 | 5 | /// The socket trait. 6 | /// 7 | /// This is the trait that needs to be implemented for a type to be used as a socket/transport. 8 | pub trait Socket: core::fmt::Debug { 9 | /// The read half of the socket. 10 | type ReadHalf: ReadHalf; 11 | /// The write half of the socket. 12 | type WriteHalf: WriteHalf; 13 | 14 | /// Split the socket into read and write halves. 15 | fn split(self) -> (Self::ReadHalf, Self::WriteHalf); 16 | } 17 | 18 | /// The read half of a socket. 19 | pub trait ReadHalf: core::fmt::Debug { 20 | /// Read from a socket. 21 | /// 22 | /// On completion, the number of bytes read is returned. 23 | /// 24 | /// Notes for implementers: 25 | /// 26 | /// * The future returned by this method must be cancel safe. 27 | /// * While there is no explicit `Unpin` bound on the future returned by this method, it is 28 | /// expected that it provides the same guarentees as `Unpin` would require. The reason `Unpin` 29 | /// is not explicitly requied is that it would force boxing (and therefore allocation) on the 30 | /// implemention that use `async fn`, which is undesirable for embedded use cases. See [this 31 | /// issue](https://github.com/rust-lang/rust/issues/82187) for details. 32 | fn read(&mut self, buf: &mut [u8]) -> impl Future>; 33 | } 34 | 35 | /// The write half of a socket. 36 | pub trait WriteHalf: core::fmt::Debug { 37 | /// Write to the socket. 38 | /// 39 | /// The returned future has the same requirements as that of [`ReadHalf::read`]. 40 | fn write(&mut self, buf: &[u8]) -> impl Future>; 41 | } 42 | 43 | /// Documentation-only socket implementations for doc tests. 44 | /// 45 | /// These types exist only to make doc tests compile and should never be used in real code. 46 | #[doc(hidden)] 47 | pub mod impl_for_doc { 48 | 49 | /// A mock socket for documentation examples. 50 | #[derive(Debug)] 51 | pub struct Socket; 52 | 53 | impl super::Socket for Socket { 54 | type ReadHalf = ReadHalf; 55 | type WriteHalf = WriteHalf; 56 | 57 | fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { 58 | (ReadHalf, WriteHalf) 59 | } 60 | } 61 | 62 | /// A mock read half for documentation examples. 63 | #[derive(Debug)] 64 | pub struct ReadHalf; 65 | 66 | impl super::ReadHalf for ReadHalf { 67 | async fn read(&mut self, _buf: &mut [u8]) -> crate::Result { 68 | unreachable!("This is only for doc tests") 69 | } 70 | } 71 | 72 | /// A mock write half for documentation examples. 73 | #[derive(Debug)] 74 | pub struct WriteHalf; 75 | 76 | impl super::WriteHalf for WriteHalf { 77 | async fn write(&mut self, _buf: &[u8]) -> crate::Result<()> { 78 | unreachable!("This is only for doc tests") 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /zlink-core/src/connection/write_connection.rs: -------------------------------------------------------------------------------- 1 | //! Contains connection related API. 2 | 3 | use core::fmt::Debug; 4 | 5 | use mayheap::Vec; 6 | use serde::Serialize; 7 | 8 | use super::{socket::WriteHalf, Call, Reply, BUFFER_SIZE}; 9 | 10 | /// A connection. 11 | /// 12 | /// The low-level API to send messages. 13 | /// 14 | /// # Cancel safety 15 | /// 16 | /// All async methods of this type are cancel safe unless explicitly stated otherwise in its 17 | /// documentation. 18 | #[derive(Debug)] 19 | pub struct WriteConnection { 20 | socket: Write, 21 | buffer: Vec, 22 | pos: usize, 23 | id: usize, 24 | } 25 | 26 | impl WriteConnection { 27 | /// Create a new connection. 28 | pub(super) fn new(socket: Write, id: usize) -> Self { 29 | Self { 30 | socket, 31 | id, 32 | buffer: Vec::from_slice(&[0; BUFFER_SIZE]).unwrap(), 33 | pos: 0, 34 | } 35 | } 36 | 37 | /// The unique identifier of the connection. 38 | #[inline] 39 | pub fn id(&self) -> usize { 40 | self.id 41 | } 42 | 43 | /// Sends a method call. 44 | /// 45 | /// The generic `Method` is the type of the method name and its input parameters. This should be 46 | /// a type that can serialize itself to a complete method call message, i-e an object containing 47 | /// `method` and `parameter` fields. This can be easily achieved using the `serde::Serialize` 48 | /// derive: 49 | /// 50 | /// ```rust 51 | /// use serde::{Serialize, Deserialize}; 52 | /// 53 | /// #[derive(Debug, Serialize, Deserialize)] 54 | /// #[serde(tag = "method", content = "parameters")] 55 | /// enum MyMethods<'m> { 56 | /// // The name needs to be the fully-qualified name of the error. 57 | /// #[serde(rename = "org.example.ftl.Alpha")] 58 | /// Alpha { param1: u32, param2: &'m str}, 59 | /// #[serde(rename = "org.example.ftl.Bravo")] 60 | /// Bravo, 61 | /// #[serde(rename = "org.example.ftl.Charlie")] 62 | /// Charlie { param1: &'m str }, 63 | /// } 64 | /// ``` 65 | pub async fn send_call(&mut self, call: &Call) -> crate::Result<()> 66 | where 67 | Method: Serialize + Debug, 68 | { 69 | trace!("connection {}: sending call: {:?}", self.id, call); 70 | self.write(call).await 71 | } 72 | 73 | /// Send a reply over the socket. 74 | /// 75 | /// The generic parameter `Params` is the type of the successful reply. This should be a type 76 | /// that can serialize itself as the `parameters` field of the reply. 77 | pub async fn send_reply(&mut self, reply: &Reply) -> crate::Result<()> 78 | where 79 | Params: Serialize + Debug, 80 | { 81 | trace!("connection {}: sending reply: {:?}", self.id, reply); 82 | self.write(reply).await 83 | } 84 | 85 | /// Send an error reply over the socket. 86 | /// 87 | /// The generic parameter `ReplyError` is the type of the error reply. This should be a type 88 | /// that can serialize itself to the whole reply object, containing `error` and `parameter` 89 | /// fields. This can be easily achieved using the `serde::Serialize` derive (See the code 90 | /// snippet in [`super::ReadConnection::receive_reply`] documentation for an example). 91 | pub async fn send_error(&mut self, error: &ReplyError) -> crate::Result<()> 92 | where 93 | ReplyError: Serialize + Debug, 94 | { 95 | trace!("connection {}: sending error: {:?}", self.id, error); 96 | self.write(error).await 97 | } 98 | 99 | /// Enqueue a call to be sent over the socket. 100 | /// 101 | /// Similar to [`WriteConnection::send_call`], except that the call is not sent immediately but 102 | /// enqueued for later sending. This is useful when you want to send multiple calls in a 103 | /// batch. 104 | pub fn enqueue_call(&mut self, call: &Call) -> crate::Result<()> 105 | where 106 | Method: Serialize + Debug, 107 | { 108 | trace!("connection {}: enqueuing call: {:?}", self.id, call); 109 | self.enqueue(call) 110 | } 111 | 112 | /// Send out the enqueued calls. 113 | pub async fn flush(&mut self) -> crate::Result<()> { 114 | if self.pos == 0 { 115 | return Ok(()); 116 | } 117 | 118 | trace!("connection {}: flushing {} bytes", self.id, self.pos); 119 | self.socket.write(&self.buffer[..self.pos]).await?; 120 | self.pos = 0; 121 | Ok(()) 122 | } 123 | 124 | async fn write(&mut self, value: &T) -> crate::Result<()> 125 | where 126 | T: Serialize + ?Sized + Debug, 127 | { 128 | self.enqueue(value)?; 129 | self.flush().await 130 | } 131 | 132 | fn enqueue(&mut self, value: &T) -> crate::Result<()> 133 | where 134 | T: Serialize + ?Sized + Debug, 135 | { 136 | let len = loop { 137 | match to_slice_at_pos(value, &mut self.buffer, self.pos) { 138 | Ok(len) => break len, 139 | #[cfg(feature = "std")] 140 | Err(crate::Error::Json(e)) if e.is_io() => { 141 | // This can only happens if `serde-json` failed to write all bytes and that 142 | // means we're running out of space or already are out of space. 143 | self.grow_buffer()?; 144 | } 145 | Err(e) => return Err(e), 146 | } 147 | }; 148 | 149 | // Add null terminator after this message. 150 | if self.pos + len == self.buffer.len() { 151 | #[cfg(feature = "std")] 152 | { 153 | self.grow_buffer()?; 154 | } 155 | #[cfg(not(feature = "std"))] 156 | { 157 | return Err(crate::Error::BufferOverflow); 158 | } 159 | } 160 | self.buffer[self.pos + len] = b'\0'; 161 | self.pos += len + 1; 162 | Ok(()) 163 | } 164 | 165 | #[cfg(feature = "std")] 166 | fn grow_buffer(&mut self) -> crate::Result<()> { 167 | if self.buffer.len() >= super::MAX_BUFFER_SIZE { 168 | return Err(crate::Error::BufferOverflow); 169 | } 170 | 171 | self.buffer.extend_from_slice(&[0; BUFFER_SIZE])?; 172 | 173 | Ok(()) 174 | } 175 | } 176 | 177 | fn to_slice_at_pos(value: &T, buf: &mut [u8], pos: usize) -> crate::Result 178 | where 179 | T: Serialize + ?Sized, 180 | { 181 | #[cfg(feature = "std")] 182 | { 183 | let mut cursor = std::io::Cursor::new(&mut buf[pos..]); 184 | serde_json::to_writer(&mut cursor, value)?; 185 | 186 | Ok(cursor.position() as usize) 187 | } 188 | 189 | #[cfg(not(feature = "std"))] 190 | { 191 | serde_json_core::to_slice(value, &mut buf[pos..]).map_err(Into::into) 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | use super::*; 198 | 199 | #[derive(Debug)] 200 | struct TestWriteHalf(usize); 201 | 202 | impl WriteHalf for TestWriteHalf { 203 | async fn write(&mut self, value: &[u8]) -> crate::Result<()> { 204 | assert_eq!(value.len(), self.0); 205 | Ok(()) 206 | } 207 | } 208 | 209 | #[tokio::test] 210 | async fn test_write() { 211 | const WRITE_LEN: usize = 212 | // Every `0u8` is one byte. 213 | BUFFER_SIZE + 214 | // `,` separators. 215 | (BUFFER_SIZE - 1) + 216 | // `[` and `]`. 217 | 2 + 218 | // null byte from enqueue. 219 | 1; 220 | let mut write_conn = WriteConnection::new(TestWriteHalf(WRITE_LEN), 1); 221 | // An item that serializes into `> BUFFER_SIZE * 2` bytes. 222 | let item: Vec = Vec::from_slice(&[0u8; BUFFER_SIZE]).unwrap(); 223 | let res = write_conn.write(&item).await; 224 | #[cfg(feature = "std")] 225 | { 226 | res.unwrap(); 227 | assert_eq!(write_conn.buffer.len(), BUFFER_SIZE * 3); 228 | assert_eq!(write_conn.pos, 0); // Reset after flush. 229 | } 230 | #[cfg(feature = "embedded")] 231 | { 232 | assert!(matches!( 233 | res, 234 | Err(crate::Error::JsonSerialize( 235 | serde_json_core::ser::Error::BufferFull 236 | )) 237 | )); 238 | assert_eq!(write_conn.buffer.len(), BUFFER_SIZE); 239 | } 240 | } 241 | 242 | #[tokio::test] 243 | async fn test_enqueue_and_flush() { 244 | // Test enqueuing multiple small items. 245 | let mut write_conn = WriteConnection::new(TestWriteHalf(5), 1); // "42\03\0" 246 | 247 | write_conn.enqueue(&42u32).unwrap(); 248 | write_conn.enqueue(&3u32).unwrap(); 249 | assert_eq!(write_conn.pos, 5); // "42\03\0" 250 | 251 | write_conn.flush().await.unwrap(); 252 | assert_eq!(write_conn.pos, 0); // Reset after flush. 253 | } 254 | 255 | #[tokio::test] 256 | async fn test_enqueue_null_terminators() { 257 | // Test that null terminators are properly placed. 258 | let mut write_conn = WriteConnection::new(TestWriteHalf(4), 1); // "1\02\0" 259 | 260 | write_conn.enqueue(&1u32).unwrap(); 261 | assert_eq!(write_conn.buffer[write_conn.pos - 1], b'\0'); 262 | 263 | write_conn.enqueue(&2u32).unwrap(); 264 | assert_eq!(write_conn.buffer[write_conn.pos - 1], b'\0'); 265 | 266 | write_conn.flush().await.unwrap(); 267 | } 268 | 269 | #[cfg(feature = "std")] 270 | #[tokio::test] 271 | async fn test_enqueue_buffer_extension() { 272 | // Test buffer extension when enqueuing large items. 273 | let mut write_conn = WriteConnection::new(TestWriteHalf(0), 1); 274 | let initial_len = write_conn.buffer.len(); 275 | 276 | // Fill up the buffer. 277 | let large_item: Vec = Vec::from_slice(&[0u8; BUFFER_SIZE]).unwrap(); 278 | write_conn.enqueue(&large_item).unwrap(); 279 | 280 | assert!(write_conn.buffer.len() > initial_len); 281 | } 282 | 283 | #[cfg(not(feature = "std"))] 284 | #[tokio::test] 285 | async fn test_enqueue_buffer_overflow() { 286 | // Test buffer overflow error without std feature. 287 | let mut write_conn = WriteConnection::new(TestWriteHalf(0), 1); 288 | 289 | // Try to enqueue an item that doesn't fit. 290 | let large_item: Vec = Vec::from_slice(&[0u8; BUFFER_SIZE]).unwrap(); 291 | let res = write_conn.enqueue(&large_item); 292 | 293 | assert!(matches!( 294 | res, 295 | Err(crate::Error::JsonSerialize( 296 | serde_json_core::ser::Error::BufferFull 297 | )) 298 | )); 299 | } 300 | 301 | #[tokio::test] 302 | async fn test_flush_empty_buffer() { 303 | // Test that flushing an empty buffer is a no-op. 304 | let mut write_conn = WriteConnection::new(TestWriteHalf(0), 1); 305 | 306 | // Should not call write since buffer is empty. 307 | write_conn.flush().await.unwrap(); 308 | assert_eq!(write_conn.pos, 0); 309 | } 310 | 311 | #[tokio::test] 312 | async fn test_multiple_flushes() { 313 | // Test multiple flushes in a row. 314 | let mut write_conn = WriteConnection::new(TestWriteHalf(2), 1); // "1\0" 315 | 316 | write_conn.enqueue(&1u32).unwrap(); 317 | write_conn.flush().await.unwrap(); 318 | assert_eq!(write_conn.pos, 0); 319 | 320 | // Second flush should be a no-op. 321 | write_conn.flush().await.unwrap(); 322 | assert_eq!(write_conn.pos, 0); 323 | } 324 | 325 | #[tokio::test] 326 | async fn test_enqueue_after_flush() { 327 | // Test that enqueuing works properly after a flush. 328 | let mut write_conn = WriteConnection::new(TestWriteHalf(2), 1); // "2\0" 329 | 330 | write_conn.enqueue(&1u32).unwrap(); 331 | write_conn.flush().await.unwrap(); 332 | 333 | // Should be able to enqueue again after flush. 334 | write_conn.enqueue(&2u32).unwrap(); 335 | assert_eq!(write_conn.pos, 2); // "2\0" 336 | 337 | write_conn.flush().await.unwrap(); 338 | assert_eq!(write_conn.pos, 0); 339 | } 340 | 341 | #[tokio::test] 342 | async fn test_call_pipelining() { 343 | use super::super::Call; 344 | use serde::{Deserialize, Serialize}; 345 | 346 | #[derive(Debug, Serialize, Deserialize)] 347 | struct TestMethod { 348 | name: &'static str, 349 | value: u32, 350 | } 351 | 352 | let mut write_conn = WriteConnection::new(TestWriteHalf(0), 1); 353 | 354 | // Test pipelining multiple method calls. 355 | let call1 = Call::new(TestMethod { 356 | name: "method1", 357 | value: 1, 358 | }); 359 | write_conn.enqueue_call(&call1).unwrap(); 360 | 361 | let call2 = Call::new(TestMethod { 362 | name: "method2", 363 | value: 2, 364 | }); 365 | write_conn.enqueue_call(&call2).unwrap(); 366 | 367 | let call3 = Call::new(TestMethod { 368 | name: "method3", 369 | value: 3, 370 | }); 371 | write_conn.enqueue_call(&call3).unwrap(); 372 | 373 | assert!(write_conn.pos > 0); 374 | 375 | // Verify that all calls are properly queued with null terminators. 376 | let buffer = &write_conn.buffer[..write_conn.pos]; 377 | let mut null_positions = [0usize; 3]; 378 | let mut null_count = 0; 379 | 380 | for (i, &byte) in buffer.iter().enumerate() { 381 | if byte == b'\0' { 382 | assert!(null_count < 3, "Found more than 3 null terminators"); 383 | null_positions[null_count] = i; 384 | null_count += 1; 385 | } 386 | } 387 | 388 | // Should have exactly 3 null terminators for 3 calls. 389 | assert_eq!(null_count, 3); 390 | 391 | // Verify each null terminator is at the end of a complete JSON object. 392 | for i in 0..null_count { 393 | let pos = null_positions[i]; 394 | assert!( 395 | pos > 0, 396 | "Null terminator at position {} should not be at start", 397 | pos 398 | ); 399 | let preceding_byte = buffer[pos - 1]; 400 | assert!( 401 | preceding_byte == b'}' || preceding_byte == b'"' || preceding_byte.is_ascii_digit(), 402 | "Null terminator at position {} should be after valid JSON ending, found byte: {}", 403 | pos, 404 | preceding_byte 405 | ); 406 | } 407 | 408 | // Verify the last null terminator is at the very end. 409 | assert_eq!(null_positions[2], write_conn.pos - 1); 410 | } 411 | 412 | #[tokio::test] 413 | async fn test_pipelining_vs_individual_sends() { 414 | use super::super::Call; 415 | use core::cell::Cell; 416 | use serde::{Deserialize, Serialize}; 417 | 418 | #[derive(Debug, Serialize, Deserialize)] 419 | struct TestMethod { 420 | operation: &'static str, 421 | id: u32, 422 | } 423 | 424 | // Track number of write calls. 425 | #[derive(Debug)] 426 | struct CountingWriteHalf { 427 | write_count: Cell, 428 | } 429 | 430 | impl WriteHalf for CountingWriteHalf { 431 | async fn write(&mut self, _value: &[u8]) -> crate::Result<()> { 432 | self.write_count.set(self.write_count.get() + 1); 433 | Ok(()) 434 | } 435 | } 436 | 437 | // Test individual sends (3 write calls expected). 438 | let counting_write = CountingWriteHalf { 439 | write_count: Cell::new(0), 440 | }; 441 | let mut write_conn_individual = WriteConnection::new(counting_write, 1); 442 | 443 | for i in 1..=3 { 444 | let call = Call::new(TestMethod { 445 | operation: "fetch", 446 | id: i, 447 | }); 448 | write_conn_individual.send_call(&call).await.unwrap(); 449 | } 450 | assert_eq!(write_conn_individual.socket.write_count.get(), 3); 451 | 452 | // Test pipelined sends (1 write call expected). 453 | let counting_write = CountingWriteHalf { 454 | write_count: Cell::new(0), 455 | }; 456 | let mut write_conn_pipelined = WriteConnection::new(counting_write, 2); 457 | 458 | for i in 1..=3 { 459 | let call = Call::new(TestMethod { 460 | operation: "fetch", 461 | id: i, 462 | }); 463 | write_conn_pipelined.enqueue_call(&call).unwrap(); 464 | } 465 | write_conn_pipelined.flush().await.unwrap(); 466 | assert_eq!(write_conn_pipelined.socket.write_count.get(), 1); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /zlink-core/src/error.rs: -------------------------------------------------------------------------------- 1 | use core::str::Utf8Error; 2 | 3 | /// The Error type for the zlink crate. 4 | #[derive(Debug)] 5 | #[non_exhaustive] 6 | pub enum Error { 7 | /// An error occurred while reading from the socket. 8 | SocketRead, 9 | /// An error occurred while writing to the socket. 10 | SocketWrite, 11 | /// Buffer overflow. 12 | BufferOverflow, 13 | /// Invalid UTF-8 data. 14 | InvalidUtf8(Utf8Error), 15 | /// Error serializing or deserializing to/from JSON. 16 | #[cfg(feature = "std")] 17 | Json(serde_json::Error), 18 | /// Error serialization to JSON. 19 | #[cfg(not(feature = "std"))] 20 | JsonSerialize(serde_json_core::ser::Error), 21 | /// Error deserialization from JSON. 22 | #[cfg(not(feature = "std"))] 23 | JsonDeserialize(serde_json_core::de::Error), 24 | /// An I/O error. 25 | #[cfg(feature = "std")] 26 | Io(std::io::Error), 27 | } 28 | 29 | /// The Result type for the zlink crate. 30 | pub type Result = core::result::Result; 31 | 32 | impl core::error::Error for Error { 33 | fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { 34 | match self { 35 | #[cfg(feature = "std")] 36 | Error::Json(e) => Some(e), 37 | #[cfg(not(feature = "std"))] 38 | Error::JsonSerialize(e) => Some(e), 39 | #[cfg(not(feature = "std"))] 40 | Error::JsonDeserialize(e) => Some(e), 41 | #[cfg(feature = "std")] 42 | Error::Io(e) => Some(e), 43 | Error::InvalidUtf8(e) => Some(e), 44 | _ => None, 45 | } 46 | } 47 | } 48 | 49 | #[cfg(feature = "std")] 50 | impl From for Error { 51 | fn from(e: serde_json::Error) -> Self { 52 | Error::Json(e) 53 | } 54 | } 55 | 56 | #[cfg(not(feature = "std"))] 57 | impl From for Error { 58 | fn from(e: serde_json_core::ser::Error) -> Self { 59 | Error::JsonSerialize(e) 60 | } 61 | } 62 | 63 | #[cfg(not(feature = "std"))] 64 | impl From for Error { 65 | fn from(e: serde_json_core::de::Error) -> Self { 66 | Error::JsonDeserialize(e) 67 | } 68 | } 69 | 70 | #[cfg(feature = "std")] 71 | impl From for Error { 72 | fn from(e: std::io::Error) -> Self { 73 | Error::Io(e) 74 | } 75 | } 76 | 77 | impl From for Error { 78 | fn from(e: mayheap::Error) -> Self { 79 | match e { 80 | mayheap::Error::BufferOverflow => Error::BufferOverflow, 81 | mayheap::Error::Utf8Error(e) => Error::InvalidUtf8(e), 82 | } 83 | } 84 | } 85 | 86 | impl core::fmt::Display for Error { 87 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 88 | match self { 89 | Error::SocketRead => write!(f, "An error occurred while reading from the socket"), 90 | Error::SocketWrite => write!(f, "An error occurred while writing to the socket"), 91 | Error::BufferOverflow => write!(f, "Buffer overflow"), 92 | Error::InvalidUtf8(e) => write!(f, "Invalid UTF-8 data: {e}"), 93 | #[cfg(feature = "std")] 94 | Error::Json(e) => write!(f, "Error serializing or deserializing to/from JSON: {e}"), 95 | #[cfg(not(feature = "std"))] 96 | Error::JsonSerialize(e) => write!(f, "Error serializing to JSON: {e}"), 97 | #[cfg(not(feature = "std"))] 98 | Error::JsonDeserialize(e) => write!(f, "Error deserializing from JSON: {e}"), 99 | #[cfg(feature = "std")] 100 | Error::Io(e) => write!(f, "I/O error: {e}"), 101 | } 102 | } 103 | } 104 | 105 | #[cfg(feature = "defmt")] 106 | impl defmt::Format for Error { 107 | fn format(&self, fmt: defmt::Formatter<'_>) { 108 | match self { 109 | Error::SocketRead => { 110 | defmt::write!(fmt, "An error occurred while reading from the socket") 111 | } 112 | Error::SocketWrite => { 113 | defmt::write!(fmt, "An error occurred while writing to the socket") 114 | } 115 | Error::BufferOverflow => defmt::write!(fmt, "Buffer overflow"), 116 | Error::InvalidUtf8(_) => defmt::write!(fmt, "Invalid UTF-8 data"), 117 | #[cfg(feature = "std")] 118 | Error::Json(_) => { 119 | defmt::write!(fmt, "Error serializing or deserializing to/from JSON") 120 | } 121 | #[cfg(not(feature = "std"))] 122 | Error::JsonSerialize(_) => defmt::write!(fmt, "Error serializing to JSON"), 123 | #[cfg(not(feature = "std"))] 124 | Error::JsonDeserialize(_) => defmt::write!(fmt, "Error deserializing from JSON"), 125 | #[cfg(feature = "std")] 126 | Error::Io(_) => defmt::write!(fmt, "I/O error"), 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /zlink-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | #![deny( 3 | missing_debug_implementations, 4 | nonstandard_style, 5 | rust_2018_idioms, 6 | missing_docs 7 | )] 8 | #![warn(unreachable_pub, clippy::std_instead_of_core)] 9 | #![doc = include_str!("../README.md")] 10 | 11 | #[cfg(all(not(feature = "std"), not(feature = "embedded")))] 12 | compile_error!("Either 'std' or 'embedded' feature must be enabled."); 13 | 14 | #[macro_use] 15 | mod log; 16 | 17 | pub mod connection; 18 | pub use connection::Connection; 19 | mod error; 20 | pub use error::{Error, Result}; 21 | mod server; 22 | pub use server::{ 23 | listener::Listener, 24 | service::{self, Service}, 25 | Server, 26 | }; 27 | mod call; 28 | pub use call::Call; 29 | pub mod reply; 30 | pub use reply::Reply; 31 | -------------------------------------------------------------------------------- /zlink-core/src/log.rs: -------------------------------------------------------------------------------- 1 | //! Logging macros that abstract `tracing` and `defmt` one. 2 | //! 3 | //! Since these macros are internal API, we only have ones that we need. 4 | 5 | #[cfg(feature = "tracing")] 6 | macro_rules! warn { 7 | ($($arg:tt)*) => { 8 | tracing::warn!($($arg)*) 9 | } 10 | } 11 | // Note: Since user has to enable either `tracing` or `defmt` feature, we can assume that `defmt` is 12 | // enabled when `tracing` is not. 13 | #[cfg(not(feature = "tracing"))] 14 | macro_rules! warn { 15 | ($($arg:tt)*) => { 16 | defmt::warn!($($arg)*) 17 | } 18 | } 19 | 20 | #[cfg(feature = "tracing")] 21 | macro_rules! trace { 22 | ($($arg:tt)*) => { 23 | tracing::trace!($($arg)*) 24 | } 25 | } 26 | #[cfg(not(feature = "tracing"))] 27 | macro_rules! trace { 28 | ($($arg:tt)*) => { 29 | defmt::trace!($($arg)*) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /zlink-core/src/reply.rs: -------------------------------------------------------------------------------- 1 | //! Method reply API. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// A successful method call reply. 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct Reply { 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub(super) parameters: Option, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub(super) continues: Option, 12 | } 13 | 14 | impl Reply { 15 | /// Create a new reply. 16 | pub fn new(parameters: Option) -> Self { 17 | Self { 18 | parameters, 19 | continues: None, 20 | } 21 | } 22 | 23 | /// Set the continues flag. 24 | pub fn set_continues(mut self, continues: Option) -> Self { 25 | self.continues = continues; 26 | self 27 | } 28 | 29 | /// The parameters of the reply. 30 | pub fn parameters(&self) -> Option<&Params> { 31 | self.parameters.as_ref() 32 | } 33 | 34 | /// Convert the reply into its parameters. 35 | pub fn into_parameters(self) -> Option { 36 | self.parameters 37 | } 38 | 39 | /// If there are more replies to come. 40 | pub fn continues(&self) -> Option { 41 | self.continues 42 | } 43 | } 44 | 45 | impl From for Reply { 46 | fn from(parameters: Params) -> Self { 47 | Self::new(Some(parameters)) 48 | } 49 | } 50 | 51 | /// A reply result. 52 | pub type Result = core::result::Result, Error>; 53 | -------------------------------------------------------------------------------- /zlink-core/src/server/listener.rs: -------------------------------------------------------------------------------- 1 | use core::future::Future; 2 | 3 | use crate::{connection::Socket, Connection, Result}; 4 | 5 | /// A listener is a server that listens for incoming connections. 6 | pub trait Listener: core::fmt::Debug { 7 | /// The type of the socket the connections this listener creates will use. 8 | type Socket: Socket; 9 | 10 | /// Accept a new connection. 11 | fn accept(&mut self) -> impl Future>>; 12 | } 13 | -------------------------------------------------------------------------------- /zlink-core/src/server/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod listener; 2 | mod select_all; 3 | pub mod service; 4 | 5 | use futures_util::{FutureExt, StreamExt}; 6 | use mayheap::Vec; 7 | use select_all::SelectAll; 8 | use service::MethodReply; 9 | 10 | use crate::{ 11 | connection::{ReadConnection, Socket, WriteConnection}, 12 | Call, Connection, Reply, 13 | }; 14 | 15 | /// A server. 16 | /// 17 | /// The server listens for incoming connections and handles method calls using a service. 18 | #[derive(Debug)] 19 | pub struct Server { 20 | listener: Option, 21 | service: Service, 22 | } 23 | 24 | impl Server 25 | where 26 | Listener: listener::Listener, 27 | Service: service::Service, 28 | { 29 | /// Create a new server that serves `service` to incomming connections from `listener`. 30 | pub fn new(listener: Listener, service: Service) -> Self { 31 | Self { 32 | listener: Some(listener), 33 | service, 34 | } 35 | } 36 | 37 | /// Run the server. 38 | /// 39 | /// # Caveats 40 | /// 41 | /// Due to [a bug in the rust compiler][abrc], the future returned by this method can not be 42 | /// treated as `Send`, even if all the specific types involved are `Send`. A major consequence 43 | /// of this fact unfortunately, is that it can not be spawned in a task of a multi-threaded 44 | /// runtime. For example, you can not currently do `tokio::spawn(server.run())`. 45 | /// 46 | /// Fortunately, there are easy workarounds for this. You can either: 47 | /// 48 | /// * Use a thread-local runtime (for example [`tokio::runtime::LocalRuntime`] or 49 | /// [`tokio::task::LocalSet`]) to run the server in a local task, perhaps in a seprate thread. 50 | /// * Use some common API to run multiple futures at once, such as [`futures::select!`] or 51 | /// [`tokio::select!`]. 52 | /// 53 | /// Most importantly, this is most likely a temporary issue and will be fixed in the future. 😊 54 | /// 55 | /// [abrc]: https://github.com/rust-lang/rust/issues/100013 56 | /// [`tokio::runtime::LocalRuntime`]: https://docs.rs/tokio/latest/tokio/runtime/struct.LocalRuntime.html 57 | /// [`tokio::task::LocalSet`]: https://docs.rs/tokio/latest/tokio/task/struct.LocalSet.html 58 | /// [`futures::select!`]: https://docs.rs/futures/latest/futures/macro.select.html 59 | /// [`tokio::select!`]: https://docs.rs/tokio/latest/tokio/macro.select.html 60 | pub async fn run(mut self) -> crate::Result<()> { 61 | let mut listener = self.listener.take().unwrap(); 62 | let mut readers = Vec::<_, MAX_CONNECTIONS>::new(); 63 | let mut writers = Vec::<_, MAX_CONNECTIONS>::new(); 64 | let mut reply_streams = 65 | Vec::, MAX_CONNECTIONS>::new(); 66 | let mut last_reply_stream_winner = None; 67 | let mut last_method_call_winner = None; 68 | 69 | loop { 70 | let mut reply_stream_futures: Vec<_, MAX_CONNECTIONS> = 71 | reply_streams.iter_mut().map(|s| s.stream.next()).collect(); 72 | let start_index = last_reply_stream_winner.map(|idx| idx + 1); 73 | let mut reply_stream_select_all = SelectAll::new(start_index); 74 | for future in reply_stream_futures.iter_mut() { 75 | reply_stream_select_all 76 | .push(future) 77 | .map_err(|_| crate::Error::BufferOverflow)?; 78 | } 79 | 80 | futures_util::select_biased! { 81 | // 1. Accept a new connection. 82 | conn = listener.accept().fuse() => { 83 | let conn = conn?; 84 | let (read, write) = conn.split(); 85 | readers 86 | .push(read) 87 | .map_err(|_| crate::Error::BufferOverflow)?; 88 | writers 89 | .push(write) 90 | .map_err(|_| crate::Error::BufferOverflow)?; 91 | } 92 | // 2. Read method calls from the existing connections and handle them. 93 | res = self.get_next_call( 94 | // SAFETY: `readers` is not invalidated or dropped until the output of this 95 | // future is dropped. 96 | unsafe { &mut *(&mut readers as *mut _) }, 97 | last_method_call_winner.map(|idx| idx + 1), 98 | ).fuse() => { 99 | let (idx, call) = res?; 100 | last_method_call_winner = Some(idx); 101 | 102 | let mut stream = None; 103 | let mut remove = true; 104 | match call { 105 | Ok(call) => match self.handle_call(call, &mut writers[idx]).await { 106 | Ok(None) => remove = false, 107 | Ok(Some(s)) => stream = Some(s), 108 | Err(e) => warn!("Error writing to connection: {:?}", e), 109 | }, 110 | Err(e) => warn!("Error reading from socket: {:?}", e), 111 | } 112 | 113 | if stream.is_some() || remove { 114 | let reader = readers.remove(idx); 115 | let writer = writers.remove(idx); 116 | 117 | #[cfg(feature = "embedded")] 118 | drop(reply_stream_futures); 119 | if let Some(stream) = stream.map(|s| ReplyStream::new(s, reader, writer)) { 120 | reply_streams 121 | .push(stream) 122 | .map_err(|_| crate::Error::BufferOverflow)?; 123 | } 124 | } 125 | } 126 | // 3. Read replies from the reply streams and send them off. 127 | reply = reply_stream_select_all.fuse() => { 128 | #[cfg(feature = "embedded")] 129 | drop(reply_stream_futures); 130 | let (idx, reply) = reply; 131 | last_reply_stream_winner = Some(idx); 132 | let id = reply_streams.get(idx).unwrap().conn.id(); 133 | 134 | match reply { 135 | Some(reply) => { 136 | if let Err(e) = reply_streams 137 | .get_mut(idx) 138 | .unwrap() 139 | .conn 140 | .write_mut() 141 | .send_reply(&reply) 142 | .await 143 | { 144 | warn!("Error writing to client {}: {:?}", id, e); 145 | reply_streams.remove(idx); 146 | } 147 | } 148 | None => { 149 | trace!("Stream closed for client {}", id); 150 | let stream = reply_streams.remove(idx); 151 | 152 | let (read, write) = stream.conn.split(); 153 | readers 154 | .push(read) 155 | .map_err(|_| crate::Error::BufferOverflow)?; 156 | writers 157 | .push(write) 158 | .map_err(|_| crate::Error::BufferOverflow)?; 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | /// Read the next method call from the connection. 167 | /// 168 | /// # Return value 169 | /// 170 | /// On success, this method returns a tuple containing: 171 | /// 172 | /// * The index of the reader that yielded a call. 173 | /// * A Result, containing a method call if reading was successful. 174 | async fn get_next_call<'r>( 175 | &mut self, 176 | readers: &'r mut Vec< 177 | ReadConnection<<::Socket as Socket>::ReadHalf>, 178 | 16, 179 | >, 180 | start_index: Option, 181 | ) -> crate::Result<(usize, crate::Result>>)> { 182 | let mut read_futures: Vec<_, 16> = readers.iter_mut().map(|r| r.receive_call()).collect(); 183 | let mut select_all = SelectAll::new(start_index); 184 | for future in &mut read_futures { 185 | // Safety: `future` is in fact `Unpin` but the compiler doesn't know that. 186 | unsafe { 187 | select_all 188 | .push_unchecked(future) 189 | .map_err(|_| crate::Error::BufferOverflow)?; 190 | } 191 | } 192 | 193 | Ok(select_all.await) 194 | } 195 | 196 | async fn handle_call( 197 | &mut self, 198 | call: Call>, 199 | writer: &mut WriteConnection<::WriteHalf>, 200 | ) -> crate::Result> { 201 | let mut stream = None; 202 | match self.service.handle(call).await { 203 | MethodReply::Single(params) => { 204 | let reply = Reply::new(params).set_continues(Some(false)); 205 | writer.send_reply(&reply).await? 206 | } 207 | MethodReply::Error(err) => writer.send_error(&err).await?, 208 | MethodReply::Multi(s) => { 209 | trace!("Client {} now turning into a reply stream", writer.id()); 210 | stream = Some(s) 211 | } 212 | } 213 | 214 | Ok(stream) 215 | } 216 | } 217 | 218 | const MAX_CONNECTIONS: usize = 16; 219 | 220 | /// Method reply stream and connection pair. 221 | #[derive(Debug)] 222 | struct ReplyStream { 223 | stream: St, 224 | conn: Connection, 225 | } 226 | 227 | impl ReplyStream 228 | where 229 | Sock: Socket, 230 | { 231 | fn new( 232 | stream: St, 233 | read_conn: ReadConnection, 234 | write_conn: WriteConnection, 235 | ) -> Self { 236 | Self { 237 | stream, 238 | conn: Connection::join(read_conn, write_conn), 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /zlink-core/src/server/select_all.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | future::Future, 3 | pin::Pin, 4 | task::{Context, Poll}, 5 | }; 6 | use mayheap::Vec; 7 | 8 | /// A future that reads from multiple futures and returns the first one that is ready. 9 | /// 10 | /// This is very similar to [`futures_util::future::SelectAll`] but much simpler and doesn't 11 | /// (necessarily) allocate. 12 | pub(super) struct SelectAll<'f, Fut> { 13 | futures: Vec, { super::MAX_CONNECTIONS }>, 14 | start_index: Option, 15 | } 16 | 17 | impl<'f, Fut> SelectAll<'f, Fut> 18 | where 19 | Fut: Future, 20 | { 21 | /// Create a new `SelectAll` with an optional starting index for round-robin polling. 22 | pub(super) fn new(start_index: Option) -> Self { 23 | SelectAll { 24 | futures: Vec::new(), 25 | start_index, 26 | } 27 | } 28 | 29 | /// Add a future to the `SelectAll`. 30 | /// 31 | /// # Safety 32 | /// 33 | /// The caller must ensure that the future is not moved/invalidated while it is in the 34 | /// `SelectAll`. The use case here is the Future impls created for `async fn` methods that are 35 | /// in reality `Unpin` but the compiler assumes they're `!Unpin`. 36 | pub(super) unsafe fn push_unchecked(&mut self, fut: &'f mut Fut) -> crate::Result<()> { 37 | self.futures 38 | .push(Pin::new_unchecked(fut)) 39 | .map_err(|_| crate::Error::BufferOverflow) 40 | } 41 | } 42 | 43 | impl<'f, Fut> SelectAll<'f, Fut> 44 | where 45 | Fut: Future + Unpin, 46 | { 47 | /// Add a future to the `SelectAll`. 48 | pub(super) fn push(&mut self, fut: &'f mut Fut) -> crate::Result<()> { 49 | self.futures 50 | .push(Pin::new(fut)) 51 | .map_err(|_| crate::Error::BufferOverflow) 52 | } 53 | } 54 | 55 | impl core::future::Future for SelectAll<'_, Fut> 56 | where 57 | Fut: Future, 58 | { 59 | type Output = (usize, Out); 60 | 61 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 62 | let num_futures = self.futures.len(); 63 | if num_futures == 0 { 64 | return Poll::Pending; 65 | } 66 | 67 | let start_idx = self.start_index.map_or(0, |idx| idx % num_futures); 68 | 69 | for i in 0..num_futures { 70 | let idx = (start_idx + i) % num_futures; 71 | if let Poll::Ready(item) = self.futures[idx].as_mut().poll(cx) { 72 | return Poll::Ready((idx, item)); 73 | } 74 | } 75 | Poll::Pending 76 | } 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use core::{ 83 | future::Future, 84 | pin::Pin, 85 | task::{Context, Poll, Waker}, 86 | }; 87 | 88 | #[test] 89 | fn round_robin_fairness() { 90 | let mut future0 = ControlledFuture::new(0); 91 | let mut future1 = ControlledFuture::new(1); 92 | let mut future2 = ControlledFuture::new(2); 93 | 94 | // Make all futures ready before adding to SelectAll. 95 | future0.set_ready(true); 96 | future1.set_ready(true); 97 | future2.set_ready(true); 98 | 99 | // Test starting from index 0. 100 | let mut select_all = SelectAll::new(Some(0)); 101 | select_all.push(&mut future0).unwrap(); 102 | select_all.push(&mut future1).unwrap(); 103 | select_all.push(&mut future2).unwrap(); 104 | 105 | let waker = dummy_waker(); 106 | let mut cx = Context::from_waker(&waker); 107 | 108 | // Should start from index 0. 109 | let pinned = Pin::new(&mut select_all); 110 | if let Poll::Ready((idx, value)) = pinned.poll(&mut cx) { 111 | assert_eq!(idx, 0); 112 | assert_eq!(value, 0); 113 | } else { 114 | panic!("Expected first future to be ready"); 115 | } 116 | } 117 | 118 | #[test] 119 | fn round_robin_with_start_index() { 120 | let mut future0 = ControlledFuture::new(0); 121 | let mut future1 = ControlledFuture::new(1); 122 | let mut future2 = ControlledFuture::new(2); 123 | 124 | // Make all futures ready before adding to SelectAll. 125 | future0.set_ready(true); 126 | future1.set_ready(true); 127 | future2.set_ready(true); 128 | 129 | // Test starting from index 1. 130 | let mut select_all = SelectAll::new(Some(1)); 131 | select_all.push(&mut future0).unwrap(); 132 | select_all.push(&mut future1).unwrap(); 133 | select_all.push(&mut future2).unwrap(); 134 | 135 | let waker = dummy_waker(); 136 | let mut cx = Context::from_waker(&waker); 137 | 138 | // Should start from index 1. 139 | let pinned = Pin::new(&mut select_all); 140 | if let Poll::Ready((idx, value)) = pinned.poll(&mut cx) { 141 | assert_eq!(idx, 1); 142 | assert_eq!(value, 1); 143 | } else { 144 | panic!("Expected second future to be ready"); 145 | } 146 | } 147 | 148 | #[test] 149 | fn round_robin_wrapping() { 150 | let mut future0 = ControlledFuture::new(0); 151 | let mut future1 = ControlledFuture::new(1); 152 | 153 | // Only make first future ready before adding to SelectAll. 154 | future0.set_ready(true); 155 | future1.set_ready(false); 156 | 157 | // Test starting from index 1, should wrap to 0. 158 | let mut select_all = SelectAll::new(Some(1)); 159 | select_all.push(&mut future0).unwrap(); 160 | select_all.push(&mut future1).unwrap(); 161 | 162 | let waker = dummy_waker(); 163 | let mut cx = Context::from_waker(&waker); 164 | 165 | // Should start from index 1, find it not ready, wrap to 0. 166 | let pinned = Pin::new(&mut select_all); 167 | if let Poll::Ready((idx, value)) = pinned.poll(&mut cx) { 168 | assert_eq!(idx, 0); 169 | assert_eq!(value, 0); 170 | } else { 171 | panic!("Expected first future to be ready after wrapping"); 172 | } 173 | } 174 | 175 | #[test] 176 | fn start_index_larger_than_futures() { 177 | let mut future0 = ControlledFuture::new(0); 178 | let mut future1 = ControlledFuture::new(1); 179 | 180 | // Make all futures ready before adding to SelectAll. 181 | future0.set_ready(true); 182 | future1.set_ready(true); 183 | 184 | // Test start index larger than number of futures. 185 | let mut select_all = SelectAll::new(Some(5)); // 5 % 2 = 1 186 | select_all.push(&mut future0).unwrap(); 187 | select_all.push(&mut future1).unwrap(); 188 | 189 | let waker = dummy_waker(); 190 | let mut cx = Context::from_waker(&waker); 191 | 192 | // Should start from index 1 (5 % 2). 193 | let pinned = Pin::new(&mut select_all); 194 | if let Poll::Ready((idx, value)) = pinned.poll(&mut cx) { 195 | assert_eq!(idx, 1); 196 | assert_eq!(value, 1); 197 | } else { 198 | panic!("Expected second future to be ready"); 199 | } 200 | } 201 | 202 | #[test] 203 | fn no_start_index_defaults_to_zero() { 204 | let mut future0 = ControlledFuture::new(0); 205 | let mut future1 = ControlledFuture::new(1); 206 | let mut future2 = ControlledFuture::new(2); 207 | 208 | // Make all futures ready before adding to SelectAll. 209 | future0.set_ready(true); 210 | future1.set_ready(true); 211 | future2.set_ready(true); 212 | 213 | // Test with None start index. 214 | let mut select_all = SelectAll::new(None); 215 | select_all.push(&mut future0).unwrap(); 216 | select_all.push(&mut future1).unwrap(); 217 | select_all.push(&mut future2).unwrap(); 218 | 219 | let waker = dummy_waker(); 220 | let mut cx = Context::from_waker(&waker); 221 | 222 | // Should start from index 0 by default. 223 | let pinned = Pin::new(&mut select_all); 224 | if let Poll::Ready((idx, value)) = pinned.poll(&mut cx) { 225 | assert_eq!(idx, 0); 226 | assert_eq!(value, 0); 227 | } else { 228 | panic!("Expected first future to be ready"); 229 | } 230 | } 231 | 232 | #[test] 233 | fn empty_select_all_returns_pending() { 234 | let mut select_all = SelectAll::::new(Some(0)); 235 | let waker = dummy_waker(); 236 | let mut cx = Context::from_waker(&waker); 237 | 238 | let pinned = Pin::new(&mut select_all); 239 | assert!(matches!(pinned.poll(&mut cx), Poll::Pending)); 240 | } 241 | 242 | #[test] 243 | fn all_futures_pending() { 244 | let mut future0 = ControlledFuture::new(0); 245 | let mut future1 = ControlledFuture::new(1); 246 | 247 | let mut select_all = SelectAll::new(Some(1)); 248 | select_all.push(&mut future0).unwrap(); 249 | select_all.push(&mut future1).unwrap(); 250 | 251 | let waker = dummy_waker(); 252 | let mut cx = Context::from_waker(&waker); 253 | 254 | // Don't make any futures ready. 255 | let pinned = Pin::new(&mut select_all); 256 | assert!(matches!(pinned.poll(&mut cx), Poll::Pending)); 257 | } 258 | 259 | /// A controllable future that can be made ready on demand. 260 | struct ControlledFuture { 261 | ready: bool, 262 | value: usize, 263 | } 264 | 265 | impl ControlledFuture { 266 | fn new(value: usize) -> Self { 267 | Self { 268 | ready: false, 269 | value, 270 | } 271 | } 272 | 273 | fn set_ready(&mut self, ready: bool) { 274 | self.ready = ready; 275 | } 276 | } 277 | 278 | impl Future for ControlledFuture { 279 | type Output = usize; 280 | 281 | fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { 282 | if self.ready { 283 | Poll::Ready(self.value) 284 | } else { 285 | Poll::Pending 286 | } 287 | } 288 | } 289 | 290 | /// Creates a dummy waker for testing. 291 | fn dummy_waker() -> Waker { 292 | use core::task::{RawWaker, RawWakerVTable}; 293 | 294 | fn dummy_raw_waker() -> RawWaker { 295 | RawWaker::new(core::ptr::null(), &VTABLE) 296 | } 297 | 298 | const VTABLE: RawWakerVTable = 299 | RawWakerVTable::new(|_| dummy_raw_waker(), |_| {}, |_| {}, |_| {}); 300 | 301 | unsafe { Waker::from_raw(dummy_raw_waker()) } 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /zlink-core/src/server/service.rs: -------------------------------------------------------------------------------- 1 | //! Serice-related API. 2 | 3 | use core::{fmt::Debug, future::Future}; 4 | 5 | use futures_util::Stream; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{Call, Reply}; 9 | 10 | /// Service trait for handling method calls. 11 | pub trait Service { 12 | /// The type of method call that this service handles. 13 | /// 14 | /// This should be a type that can deserialize itself from a complete method call message: i-e 15 | /// an object containing `method` and `parameter` fields. This can be easily achieved using the 16 | /// `serde::Deserialize` derive (See the code snippet in 17 | /// [`crate::connection::WriteConnection::send_call`] documentation for an example). 18 | type MethodCall<'de>: Deserialize<'de> + Debug; 19 | /// The type of the successful reply. 20 | /// 21 | /// This should be a type that can serialize itself as the `parameters` field of the reply. 22 | type ReplyParams<'ser>: Serialize + Debug 23 | where 24 | Self: 'ser; 25 | /// The type of the item that [`Service::ReplyStream`] will be expected to yield. 26 | /// 27 | /// This should be a type that can serialize itself as the `parameters` field of the reply. 28 | type ReplyStreamParams: Serialize + Debug; 29 | /// The type of the multi-reply stream. 30 | /// 31 | /// If the client asks for multiple replies, this stream will be used to send them. 32 | type ReplyStream: Stream> + Unpin + Debug; 33 | /// The type of the error reply. 34 | /// 35 | /// This should be a type that can serialize itself to the whole reply object, containing 36 | /// `error` and `parameter` fields. This can be easily achieved using the `serde::Serialize` 37 | /// derive (See the code snippet in [`crate::connection::ReadConnection::receive_reply`] 38 | /// documentation for an example). 39 | type ReplyError<'ser>: Serialize + Debug 40 | where 41 | Self: 'ser; 42 | 43 | /// Handle a method call. 44 | fn handle<'ser>( 45 | &'ser mut self, 46 | method: Call>, 47 | ) -> impl Future< 48 | Output = MethodReply, Self::ReplyStream, Self::ReplyError<'ser>>, 49 | >; 50 | } 51 | 52 | /// A service method call reply. 53 | #[derive(Debug)] 54 | pub enum MethodReply { 55 | /// A single reply. 56 | Single(Option), 57 | /// An error reply. 58 | Error(ReplyError), 59 | /// A multi-reply stream. 60 | Multi(ReplyStream), 61 | } 62 | -------------------------------------------------------------------------------- /zlink-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-macros" 3 | version = "0.0.1-alpha.1" 4 | description = "Macros providing the high-level zlink API" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /zlink-macros/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_debug_implementations, 3 | nonstandard_style, 4 | rust_2018_idioms, 5 | missing_docs 6 | )] 7 | #![warn(unreachable_pub)] 8 | #![doc = include_str!("../README.md")] 9 | 10 | /// Add two numbers together. 11 | pub fn add(left: u64, right: u64) -> u64 { 12 | left + right 13 | } 14 | -------------------------------------------------------------------------------- /zlink-micro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-micro" 3 | version = "0.0.1-alpha.1" 4 | description = "zlink library for microcontrollers" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /zlink-micro/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-micro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_debug_implementations, 3 | nonstandard_style, 4 | rust_2018_idioms, 5 | missing_docs 6 | )] 7 | #![warn(unreachable_pub)] 8 | #![doc = include_str!("../README.md")] 9 | 10 | /// Add two numbers together. 11 | pub fn add(left: u64, right: u64) -> u64 { 12 | left + right 13 | } 14 | -------------------------------------------------------------------------------- /zlink-tokio/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-tokio" 3 | version = "0.0.1-alpha.1" 4 | description = "zlink library for the Tokio runtime" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | zlink-core = { path = "../zlink-core", version = "0.0.1-alpha.1" } 12 | tokio = { version = "1.44.0", features = ["net", "io-util", "tracing"] } 13 | futures-util = { version = "0.3.31", default-features = false, features = [ 14 | "async-await", 15 | "alloc", 16 | ] } 17 | tokio-stream = { version = "0.1.17", default-features = false, features = [ 18 | "sync", 19 | ] } 20 | 21 | [dev-dependencies] 22 | tokio = { version = "1.44.0", features = [ 23 | "macros", 24 | "rt", 25 | "rt-multi-thread", 26 | "test-util", 27 | "fs", 28 | ] } 29 | serde = { version = "1.0.218", default-features = false, features = ["derive"] } 30 | serde_repr = "0.1.20" 31 | test-log = { version = "0.2.17", default-features = false, features = [ 32 | "trace", 33 | "color", 34 | ] } 35 | -------------------------------------------------------------------------------- /zlink-tokio/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-tokio/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_debug_implementations, 3 | nonstandard_style, 4 | rust_2018_idioms, 5 | missing_docs 6 | )] 7 | #![warn(unreachable_pub)] 8 | #![doc = include_str!("../README.md")] 9 | 10 | pub use zlink_core::*; 11 | pub mod notified; 12 | pub mod unix; 13 | -------------------------------------------------------------------------------- /zlink-tokio/src/notified.rs: -------------------------------------------------------------------------------- 1 | //! Convenience API for maintaining state, that notifies on changes. 2 | 3 | use std::{ 4 | fmt::Debug, 5 | future::Future, 6 | pin::Pin, 7 | task::{ready, Context, Poll}, 8 | }; 9 | 10 | use crate::Reply; 11 | use tokio::sync::{broadcast, oneshot}; 12 | use tokio_stream::wrappers::BroadcastStream; 13 | 14 | /// A notified state (e.g a field) of a service implementation. 15 | #[derive(Debug, Clone)] 16 | pub struct State { 17 | value: T, 18 | tx: broadcast::Sender, 19 | } 20 | 21 | impl State 22 | where 23 | T: Into + Clone + Debug, 24 | ReplyParams: Clone + Send + 'static + Debug, 25 | { 26 | /// Create a new notified field. 27 | pub fn new(value: T) -> Self { 28 | let (tx, _) = broadcast::channel(1); 29 | 30 | Self { value, tx } 31 | } 32 | 33 | /// Set the value of the notified field and notify all listeners. 34 | pub fn set(&mut self, value: T) { 35 | self.value = value.clone(); 36 | // Failure means that there are currently no receivers and that's ok. 37 | let _ = self.tx.send(value.into()); 38 | } 39 | 40 | /// Get the value of the notified field. 41 | pub fn get(&self) -> T { 42 | self.value.clone() 43 | } 44 | 45 | /// Get a stream of replies for the notified field. 46 | pub fn stream(&self) -> Stream { 47 | Stream(StreamInner::Broadcast(self.tx.subscribe().into())) 48 | } 49 | } 50 | 51 | /// A one-shot notified state of a service implementation. 52 | /// 53 | /// This is useful for handling method calls in a separate task/thread. 54 | #[derive(Debug)] 55 | pub struct Once { 56 | tx: oneshot::Sender, 57 | } 58 | 59 | impl Once 60 | where 61 | ReplyParams: Send + 'static + Debug, 62 | { 63 | /// Create a new notified oneshot state. 64 | pub fn new() -> (Self, Stream) { 65 | let (tx, rx) = oneshot::channel(); 66 | 67 | (Self { tx }, Stream(StreamInner::Oneshot(rx))) 68 | } 69 | 70 | /// Set the value of the notified field and notify all listeners. 71 | pub fn notify(self, value: T) 72 | where 73 | T: Into + Debug, 74 | { 75 | // Failure means that we dropped the receiver stream internally before it received anything 76 | // and that's a big bug that must not happen. 77 | self.tx.send(value.into()).unwrap(); 78 | } 79 | } 80 | 81 | /// The stream to use as the [`crate::Service::ReplyStream`] in service implementation when using 82 | /// [`State`] or [`Once`]. 83 | #[derive(Debug)] 84 | pub struct Stream(StreamInner); 85 | 86 | impl futures_util::Stream for Stream 87 | where 88 | ReplyParams: Clone + Send + 'static, 89 | { 90 | type Item = Reply; 91 | 92 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 93 | match &mut self.0 { 94 | StreamInner::Broadcast(stream) => { 95 | let reply = loop { 96 | match ready!(Pin::new(&mut *stream).poll_next(cx)) { 97 | Some(Ok(reply)) => { 98 | break Some(Reply::new(Some(reply)).set_continues(Some(true))); 99 | } 100 | // Some intermediate values were missed. That's OK, as long as we get the 101 | // latest value. 102 | Some(Err(_)) => continue, 103 | None => break None, 104 | } 105 | }; 106 | 107 | Poll::Ready(reply) 108 | } 109 | StreamInner::Oneshot(stream) => { 110 | if stream.is_terminated() { 111 | return Poll::Ready(None); 112 | } 113 | 114 | Pin::new(&mut *stream).poll(cx).map(|reply| { 115 | reply 116 | .map(|reply| Reply::new(Some(reply)).set_continues(Some(false))) 117 | .ok() 118 | }) 119 | } 120 | } 121 | } 122 | } 123 | 124 | #[derive(Debug)] 125 | enum StreamInner { 126 | Broadcast(BroadcastStream), 127 | Oneshot(oneshot::Receiver), 128 | } 129 | -------------------------------------------------------------------------------- /zlink-tokio/src/unix/listener.rs: -------------------------------------------------------------------------------- 1 | use crate::{Connection, Result}; 2 | 3 | /// Create a new unix domain socket listener and bind it to `path`. 4 | pub fn bind

(path: P) -> Result 5 | where 6 | P: AsRef, 7 | { 8 | tokio::net::UnixListener::bind(path) 9 | .map(|listener| Listener { listener }) 10 | .map_err(Into::into) 11 | } 12 | 13 | /// A unix domain socket listener. 14 | #[derive(Debug)] 15 | pub struct Listener { 16 | listener: tokio::net::UnixListener, 17 | } 18 | 19 | impl crate::Listener for Listener { 20 | type Socket = super::Stream; 21 | 22 | async fn accept(&mut self) -> Result> { 23 | self.listener 24 | .accept() 25 | .await 26 | .map(|(stream, _)| super::Stream::from(stream).into()) 27 | .map_err(Into::into) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /zlink-tokio/src/unix/mod.rs: -------------------------------------------------------------------------------- 1 | //! Provides transport over Unix Domain Sockets. 2 | 3 | mod stream; 4 | pub use stream::{connect, Connection, Stream}; 5 | mod listener; 6 | pub use listener::{bind, Listener}; 7 | -------------------------------------------------------------------------------- /zlink-tokio/src/unix/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | connection::socket::{self, Socket}, 3 | Result, 4 | }; 5 | use tokio::{ 6 | io::{AsyncReadExt, AsyncWriteExt}, 7 | net::{unix, UnixStream}, 8 | }; 9 | 10 | /// The connection type that uses Unix Domain Sockets for transport. 11 | pub type Connection = crate::Connection; 12 | 13 | /// Connect to Unix Domain Socket at the given path. 14 | pub async fn connect

(path: P) -> Result 15 | where 16 | P: AsRef, 17 | { 18 | UnixStream::connect(path) 19 | .await 20 | .map(Stream) 21 | .map(Connection::new) 22 | .map_err(Into::into) 23 | } 24 | 25 | /// The [`Socket`] implementation using Unix Domain Sockets. 26 | #[derive(Debug)] 27 | pub struct Stream(UnixStream); 28 | 29 | impl Socket for Stream { 30 | type ReadHalf = ReadHalf; 31 | type WriteHalf = WriteHalf; 32 | 33 | fn split(self) -> (Self::ReadHalf, Self::WriteHalf) { 34 | let (read, write) = self.0.into_split(); 35 | 36 | (ReadHalf(read), WriteHalf(write)) 37 | } 38 | } 39 | 40 | impl From for Stream { 41 | fn from(stream: UnixStream) -> Self { 42 | Self(stream) 43 | } 44 | } 45 | 46 | /// The [`ReadHalf`] implementation using Unix Domain Sockets. 47 | #[derive(Debug)] 48 | pub struct ReadHalf(unix::OwnedReadHalf); 49 | 50 | impl socket::ReadHalf for ReadHalf { 51 | async fn read(&mut self, buf: &mut [u8]) -> Result { 52 | self.0.read(buf).await.map_err(Into::into) 53 | } 54 | } 55 | 56 | /// The [`WriteHalf`] implementation using Unix Domain Sockets. 57 | #[derive(Debug)] 58 | pub struct WriteHalf(unix::OwnedWriteHalf); 59 | 60 | impl socket::WriteHalf for WriteHalf { 61 | async fn write(&mut self, buf: &[u8]) -> Result<()> { 62 | let mut pos = 0; 63 | 64 | while pos < buf.len() { 65 | let n = self.0.write(&buf[pos..]).await?; 66 | pos += n; 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /zlink-usb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink-usb" 3 | version = "0.0.1-alpha.1" 4 | description = "USB-based transport for zlink" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [dependencies] 11 | -------------------------------------------------------------------------------- /zlink-usb/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink-usb/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny( 2 | missing_debug_implementations, 3 | nonstandard_style, 4 | rust_2018_idioms, 5 | missing_docs 6 | )] 7 | #![warn(unreachable_pub)] 8 | #![doc = include_str!("../README.md")] 9 | 10 | /// Add two numbers together. 11 | pub fn add(left: u64, right: u64) -> u64 { 12 | left + right 13 | } 14 | -------------------------------------------------------------------------------- /zlink/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zlink" 3 | version = "0.0.1-alpha.1" 4 | description = "Async Varlink API" 5 | edition.workspace = true 6 | rust-version.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | 10 | [features] 11 | default = ["tokio"] 12 | tokio = ["dep:zlink-tokio"] 13 | 14 | [dependencies] 15 | zlink-tokio = { path = "../zlink-tokio", version = "0.0.1-alpha.1", default-features = false, optional = true } 16 | 17 | [dev-dependencies] 18 | tokio = { version = "1.44.0", features = [ 19 | "macros", 20 | "rt", 21 | "rt-multi-thread", 22 | "test-util", 23 | "fs", 24 | ] } 25 | serde = { version = "1.0.218", default-features = false, features = ["derive"] } 26 | serde_repr = "0.1.20" 27 | test-log = { version = "0.2.17", default-features = false, features = [ 28 | "trace", 29 | "color", 30 | ] } 31 | futures-util = { version = "0.3.31", default-features = false, features = [ 32 | "async-await", 33 | ] } 34 | -------------------------------------------------------------------------------- /zlink/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /zlink/examples/resolved.rs: -------------------------------------------------------------------------------- 1 | // Resolve a given hostname to an IP address using `systemd-resolved`'s Varlink service. 2 | // We use the low-level API to send a method call and receive a reply. 3 | use std::{env::args, fmt::Display, net::IpAddr}; 4 | 5 | use serde_repr::{Deserialize_repr, Serialize_repr}; 6 | 7 | #[tokio::main(flavor = "current_thread")] 8 | async fn main() -> Result<(), Box> { 9 | let mut connection = zlink::unix::connect("/run/systemd/resolve/io.systemd.Resolve").await?; 10 | 11 | let args: Vec<_> = args().skip(1).collect(); 12 | 13 | // First send out all the method calls (let's make use of pipelinning feature of Varlink!). 14 | for name in args.clone() { 15 | let resolve = Method::ResolveHostName { name: &name }; 16 | connection.enqueue_call(&resolve.into())?; 17 | } 18 | connection.flush().await?; 19 | 20 | // Then fetch the results and print them. 21 | for name in args.clone() { 22 | match connection 23 | .receive_reply::() 24 | .await 25 | .map(|r| r.map(|r| r.into_parameters().unwrap().addresses))? 26 | { 27 | Ok(addresses) => { 28 | println!("Results for '{}':", name); 29 | for address in addresses { 30 | println!("\t{}", address); 31 | } 32 | } 33 | Err(e) => eprintln!("Error resolving '{}': {}", name, e), 34 | } 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | #[derive(Debug, serde::Serialize)] 41 | #[serde(tag = "method", content = "parameters")] 42 | enum Method<'m> { 43 | #[serde(rename = "io.systemd.Resolve.ResolveHostname")] 44 | ResolveHostName { name: &'m str }, 45 | } 46 | 47 | #[derive(Debug, serde::Deserialize)] 48 | struct ReplyParams<'r> { 49 | addresses: Vec, 50 | #[serde(rename = "name")] 51 | _name: &'r str, 52 | } 53 | 54 | #[derive(Debug, serde::Deserialize)] 55 | struct ResolvedAddress { 56 | family: ProtocolFamily, 57 | address: Vec, 58 | } 59 | 60 | impl Display for ResolvedAddress { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | let ip = match self.family { 63 | ProtocolFamily::Inet => { 64 | let ip = <[u8; 4]>::try_from(self.address.as_slice()) 65 | .map(IpAddr::from) 66 | .unwrap(); 67 | format!("IPv4: {}", ip) 68 | } 69 | ProtocolFamily::Inet6 => { 70 | let ip = <[u8; 16]>::try_from(self.address.as_slice()) 71 | .map(IpAddr::from) 72 | .unwrap(); 73 | format!("IPv6: {}", ip) 74 | } 75 | ProtocolFamily::Unspec => { 76 | format!("Unspecified protocol family: {:?}", self.address) 77 | } 78 | }; 79 | write!(f, "{}", ip) 80 | } 81 | } 82 | 83 | #[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug)] 84 | #[repr(u8)] 85 | enum ProtocolFamily { 86 | Unspec = 0, // Unspecified. 87 | Inet = 2, // IP protocol family. 88 | Inet6 = 10, // IP version 6. 89 | } 90 | 91 | #[derive(Debug, serde::Deserialize)] 92 | #[serde(tag = "error", content = "parameters")] 93 | enum ReplyError<'e> { 94 | #[serde(rename = "io.systemd.Resolve.NoNameServers")] 95 | NoNameServers, 96 | #[serde(rename = "io.systemd.Resolve.NoSuchResourceRecord")] 97 | NoSuchResourceRecord, 98 | #[serde(rename = "io.systemd.Resolve.QueryTimedOut")] 99 | QueryTimedOut, 100 | #[serde(rename = "io.systemd.Resolve.MaxAttemptsReached")] 101 | MaxAttemptsReached, 102 | #[serde(rename = "io.systemd.Resolve.InvalidReply")] 103 | InvalidReply, 104 | #[serde(rename = "io.systemd.Resolve.QueryAborted")] 105 | QueryAborted, 106 | #[serde(rename = "io.systemd.Resolve.DNSSECValidationFailed")] 107 | DNSSECValidationFailed { 108 | #[serde(rename = "result")] 109 | _result: &'e str, 110 | #[serde(rename = "extendedDNSErrorCode")] 111 | _extended_dns_error_code: Option, 112 | #[serde(rename = "extendedDNSErrorMessage")] 113 | _extended_dns_error_message: Option<&'e str>, 114 | }, 115 | #[serde(rename = "io.systemd.Resolve.NoTrustAnchor")] 116 | NoTrustAnchor, 117 | #[serde(rename = "io.systemd.Resolve.ResourceRecordTypeUnsupported")] 118 | ResourceRecordTypeUnsupported, 119 | #[serde(rename = "io.systemd.Resolve.NetworkDown")] 120 | NetworkDown, 121 | #[serde(rename = "io.systemd.Resolve.NoSource")] 122 | NoSource, 123 | #[serde(rename = "io.systemd.Resolve.StubLoop")] 124 | StubLoop, 125 | #[serde(rename = "io.systemd.Resolve.DNSError")] 126 | DNSError { 127 | #[serde(rename = "rcode")] 128 | _rcode: i32, 129 | #[serde(rename = "extendedDNSErrorCode")] 130 | _extended_dns_error_code: Option, 131 | #[serde(rename = "extendedDNSErrorMessage")] 132 | _extended_dns_error_message: Option<&'e str>, 133 | }, 134 | #[serde(rename = "io.systemd.Resolve.CNAMELoop")] 135 | CNAMELoop, 136 | #[serde(rename = "io.systemd.Resolve.BadAddressSize")] 137 | BadAddressSize, 138 | #[serde(rename = "io.systemd.Resolve.ResourceRecordTypeInvalidForQuery")] 139 | ResourceRecordTypeInvalidForQuery, 140 | #[serde(rename = "io.systemd.Resolve.ZoneTransfersNotPermitted")] 141 | ZoneTransfersNotPermitted, 142 | #[serde(rename = "io.systemd.Resolve.ResourceRecordTypeObsolete")] 143 | ResourceRecordTypeObsolete, 144 | } 145 | 146 | impl Display for ReplyError<'_> { 147 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 148 | write!(f, "{:?}", self) 149 | } 150 | } 151 | 152 | impl std::error::Error for ReplyError<'_> {} 153 | -------------------------------------------------------------------------------- /zlink/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "tokio"), no_std)] 2 | #![deny( 3 | missing_debug_implementations, 4 | nonstandard_style, 5 | rust_2018_idioms, 6 | missing_docs 7 | )] 8 | #![warn(unreachable_pub)] 9 | #![doc = include_str!("../README.md")] 10 | 11 | #[cfg(not(feature = "tokio"))] 12 | compile_error!( 13 | "Currently 'tokio' feature must be enabled. `embassy` feature will also be supported in the future." 14 | ); 15 | 16 | #[cfg(feature = "tokio")] 17 | pub use zlink_tokio::*; 18 | -------------------------------------------------------------------------------- /zlink/tests/lowlevel-ftl.rs: -------------------------------------------------------------------------------- 1 | use std::{pin::pin, time::Duration}; 2 | 3 | use futures_util::{pin_mut, stream::StreamExt, TryStreamExt}; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio::{select, time::sleep}; 6 | use zlink::{ 7 | notified, 8 | service::MethodReply, 9 | unix::{bind, connect}, 10 | Call, Service, 11 | }; 12 | 13 | #[test_log::test(tokio::test(flavor = "multi_thread"))] 14 | async fn lowlevel_ftl() -> Result<(), Box> { 15 | // Remove the socket file if it exists (from a previous run of this test). 16 | if let Err(e) = tokio::fs::remove_file(SOCKET_PATH).await { 17 | // It's OK if the file doesn't exist. 18 | if e.kind() != std::io::ErrorKind::NotFound { 19 | return Err(e.into()); 20 | } 21 | } 22 | 23 | // The transitions between the drive conditions. 24 | let conditions = [ 25 | DriveCondition { 26 | state: DriveState::Idle, 27 | tylium_level: 100, 28 | }, 29 | DriveCondition { 30 | state: DriveState::Spooling, 31 | tylium_level: 90, 32 | }, 33 | DriveCondition { 34 | state: DriveState::Spooling, 35 | tylium_level: 90, 36 | }, 37 | ]; 38 | 39 | // Setup the server and run it in a separate task. 40 | let listener = bind(SOCKET_PATH).unwrap(); 41 | let service = Ftl::new(conditions[0]); 42 | let server = zlink::Server::new(listener, service); 43 | select! { 44 | res = server.run() => res?, 45 | res = run_client(&conditions) => res?, 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | async fn run_client(conditions: &[DriveCondition]) -> Result<(), Box> { 52 | // Now create a client connection that monitor changes in the drive condition. 53 | let mut conn = connect(SOCKET_PATH).await?; 54 | let call = Call::new(Methods::GetDriveCondition).set_more(Some(true)); 55 | let mut drive_monitor_stream = pin!( 56 | conn.chain_call::(&call)? 57 | .send() 58 | .await? 59 | ); 60 | 61 | // And a client that only calls methods. 62 | { 63 | let mut conn = connect(SOCKET_PATH).await?; 64 | 65 | // Ask for the drive condition, then set them and then ask again. 66 | let get_drive_cond = Methods::GetDriveCondition.into(); 67 | let set_drive_cond = Methods::SetDriveCondition { 68 | condition: conditions[1], 69 | } 70 | .into(); 71 | 72 | let replies = conn 73 | .chain_call::(&get_drive_cond)? 74 | .append(&set_drive_cond)? 75 | .append(&get_drive_cond)? 76 | .send() 77 | .await?; 78 | 79 | // Now we should be able to get all the replies. 80 | { 81 | pin_mut!(replies); 82 | 83 | for i in 0..3 { 84 | let reply = replies.next().await.unwrap()?.unwrap(); 85 | match reply.into_parameters().unwrap() { 86 | Replies::DriveCondition(drive_condition) => { 87 | assert_eq!(drive_condition, conditions[i]); 88 | } 89 | _ => panic!("Unexpected reply"), 90 | } 91 | } 92 | } 93 | 94 | let duration = 10; 95 | let impossible_speed = conditions[1].tylium_level / duration + 1; 96 | let replies = conn 97 | // Let's try to jump to a new coordinate but first requiring more tylium 98 | // than we have. 99 | .chain_call::<_, Replies, Errors>( 100 | &Methods::Jump { 101 | config: DriveConfiguration { 102 | speed: impossible_speed, 103 | trajectory: 1, 104 | duration: 10, 105 | }, 106 | } 107 | .into(), 108 | )? 109 | // Now let's try to jump with a valid speed. 110 | .append( 111 | &Methods::Jump { 112 | config: DriveConfiguration { 113 | speed: impossible_speed - 1, 114 | trajectory: 1, 115 | duration: 10, 116 | }, 117 | } 118 | .into(), 119 | )? 120 | .send() 121 | .await?; 122 | pin_mut!(replies); 123 | let e = replies.try_next().await?.unwrap().unwrap_err(); 124 | // The first call should fail because we didn't have enough energy. 125 | assert_eq!(e, Errors::NotEnoughEnergy); 126 | 127 | // The second call should succeed. 128 | let reply = replies.try_next().await?.unwrap()?; 129 | assert_eq!( 130 | reply.parameters(), 131 | Some(&Replies::Coordinates(Coordinate { 132 | longitude: 1.0, 133 | latitude: 0.0, 134 | distance: 10, 135 | })) 136 | ); 137 | } 138 | 139 | // `drive_monitor_conn` should have received the drive condition changes. 140 | let drive_cond = drive_monitor_stream.try_next().await?.unwrap()?; 141 | match drive_cond.parameters().unwrap() { 142 | Replies::DriveCondition(condition) => { 143 | assert_eq!(condition, &conditions[1]); 144 | } 145 | _ => panic!("Expected DriveCondition reply"), 146 | } 147 | 148 | Ok(()) 149 | } 150 | 151 | // The FTL service. 152 | struct Ftl { 153 | drive_condition: notified::State, 154 | coordinates: Coordinate, 155 | } 156 | 157 | impl Ftl { 158 | fn new(init_conditions: DriveCondition) -> Self { 159 | Self { 160 | drive_condition: notified::State::new(init_conditions), 161 | coordinates: Coordinate { 162 | longitude: 0.0, 163 | latitude: 0.0, 164 | distance: 0, 165 | }, 166 | } 167 | } 168 | } 169 | 170 | impl Service for Ftl { 171 | type MethodCall<'de> = Methods; 172 | type ReplyParams<'ser> = Replies; 173 | type ReplyStream = notified::Stream; 174 | type ReplyStreamParams = Replies; 175 | type ReplyError<'ser> = Errors; 176 | 177 | async fn handle<'ser>( 178 | &'ser mut self, 179 | call: Call>, 180 | ) -> MethodReply, Self::ReplyStream, Self::ReplyError<'ser>> { 181 | match call.method() { 182 | Methods::GetDriveCondition if call.more().unwrap_or_default() => { 183 | MethodReply::Multi(self.drive_condition.stream()) 184 | } 185 | Methods::GetDriveCondition => { 186 | MethodReply::Single(Some(self.drive_condition.get().into())) 187 | } 188 | Methods::SetDriveCondition { condition } => { 189 | if call.more().unwrap_or_default() { 190 | return MethodReply::Error(Errors::ParameterOutOfRange); 191 | } 192 | self.drive_condition.set(*condition); 193 | MethodReply::Single(Some(self.drive_condition.get().into())) 194 | } 195 | Methods::GetCoordinates => { 196 | MethodReply::Single(Some(Replies::Coordinates(self.coordinates))) 197 | } 198 | Methods::Jump { config } => { 199 | if call.more().unwrap_or_default() { 200 | return MethodReply::Error(Errors::ParameterOutOfRange); 201 | } 202 | let tylium_required = config.speed * config.duration; 203 | let mut condition = self.drive_condition.get(); 204 | if tylium_required > condition.tylium_level { 205 | return MethodReply::Error(Errors::NotEnoughEnergy); 206 | } 207 | let current_coords = self.coordinates; 208 | let config = *config; 209 | 210 | sleep(Duration::from_millis(1)).await; // Simulate spooling time. 211 | 212 | let coords = Coordinate { 213 | longitude: current_coords.longitude + config.trajectory as f32, 214 | latitude: current_coords.latitude, 215 | distance: current_coords.distance + config.duration, 216 | }; 217 | condition.state = DriveState::Idle; 218 | condition.tylium_level = condition.tylium_level - tylium_required; 219 | self.drive_condition.set(condition); 220 | self.coordinates = coords; 221 | 222 | MethodReply::Single(Some(Replies::Coordinates(coords))) 223 | } 224 | } 225 | } 226 | } 227 | 228 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] 229 | struct DriveCondition { 230 | state: DriveState, 231 | tylium_level: i64, 232 | } 233 | 234 | impl From for Replies { 235 | fn from(drive_condition: DriveCondition) -> Self { 236 | Replies::DriveCondition(drive_condition) 237 | } 238 | } 239 | 240 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] 241 | #[serde(rename_all = "snake_case")] 242 | pub enum DriveState { 243 | Idle, 244 | Spooling, 245 | Busy, 246 | } 247 | 248 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] 249 | struct DriveConfiguration { 250 | speed: i64, 251 | trajectory: i64, 252 | duration: i64, 253 | } 254 | 255 | #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] 256 | struct Coordinate { 257 | longitude: f32, 258 | latitude: f32, 259 | distance: i64, 260 | } 261 | 262 | impl From for Replies { 263 | fn from(coordinate: Coordinate) -> Self { 264 | Replies::Coordinates(coordinate) 265 | } 266 | } 267 | 268 | /// The FTL service methods. 269 | #[derive(Debug, Serialize, Deserialize)] 270 | #[serde(tag = "method", content = "parameters")] 271 | enum Methods { 272 | #[serde(rename = "org.example.ftl.GetDriveCondition")] 273 | GetDriveCondition, 274 | #[serde(rename = "org.example.ftl.SetDriveCondition")] 275 | SetDriveCondition { condition: DriveCondition }, 276 | #[serde(rename = "org.example.ftl.GetCoordinates")] 277 | GetCoordinates, 278 | #[serde(rename = "org.example.ftl.Jump")] 279 | Jump { config: DriveConfiguration }, 280 | } 281 | 282 | /// The FTL service replies. 283 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] 284 | #[serde(untagged)] 285 | enum Replies { 286 | DriveCondition(DriveCondition), 287 | Coordinates(Coordinate), 288 | } 289 | 290 | /// The FTL service error replies. 291 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 292 | #[serde(tag = "error", content = "parameters")] 293 | enum Errors { 294 | #[serde(rename = "org.example.ftl.NotEnoughEnergy")] 295 | NotEnoughEnergy, 296 | #[serde(rename = "org.example.ftl.ParameterOutOfRange")] 297 | ParameterOutOfRange, 298 | } 299 | 300 | impl core::fmt::Display for Errors { 301 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 302 | match self { 303 | Errors::NotEnoughEnergy => write!(f, "Not enough energy"), 304 | Errors::ParameterOutOfRange => write!(f, "Parameter out of range"), 305 | } 306 | } 307 | } 308 | 309 | impl std::error::Error for Errors {} 310 | 311 | const SOCKET_PATH: &'static str = "/tmp/zlink-lowlevel-ftl.sock"; 312 | --------------------------------------------------------------------------------